{
  "generated_at": "2026-09-16T14:53:25.523259+00:00",
  "source": "bot_modular@main",
  "modules": [
    {
      "name": "cookbook",
      "version": "1.0.0",
      "category": "dev",
      "description": "Эталонный модуль-справочник — показывает все ключевые паттерны разработки модулей bot_modular.",
      "requires": [],
      "rights": [],
      "api_methods": [],
      "web_routes": [],
      "commit": "2d7d04615b31be9f5bbd91d5d61671182164bf39",
      "updated": "2026-09-16 16:35:50 +0300",
      "integrity": "sha256:c265cc1b2719960864c2d0bf6049fcc70e55ac01f9bad97d0a09962c8faf930a",
      "tree_url": "http://10.10.10.30:3000/euk0r/bot_modular/src/branch/main/modules/cookbook",
      "raw_url": "http://10.10.10.30:3000/euk0r/bot_modular/raw/branch/main/modules/cookbook/module.py",
      "files": [
        {
          "path": "README.md",
          "size": 12375,
          "content": "# cookbook `1.0.0` — эталонный модуль-справочник bot_modular\n\nПолный пример всех ключевых паттернов разработки модулей. Каждый файл модуля\nпрокомментирован и объясняет свой контракт с ядром.\n\n## Жизненный цикл модуля\n\n```\nloader.discover()          → сканирует modules/*/manifest.{yaml,json}\nmodstate.is_enabled()      → default-off для новых (включить через modsafe)\nloader.load_module()       → импортирует module.py, создаёт Module()\n  ├─ manifest.validate()   → обязательные поля: name, version, description\n  ├─ requires.resolve()    → ждёт зависимости (requires: [...])\n  ├─ rights.register()     → регистрирует права из manifest.yaml\n  └─ module.setup(ctx)     # ← регистрация API, событий, UI\ntransport.start()          → после всех модулей\n  └─ module.start()        # ← фоновые потоки, подключения\nmodule.stop()              # ← при shutdown\n```\n\n## Контракты вкладов\n\n### 1. module.py — ядро модуля\n\n```python\nclass Module(BaseModule):\n    def setup(self, ctx) -> None:\n        \"\"\"Однажды при загрузке. НЕ поднимаем потоки.\"\"\"\n        self.config = ctx.module_config(\"cookbook\")  # defaults < data/env/*.env < overrides\n        self.cache = ctx.cache.ns(\"cookbook\")        # изолированный кеш\n        self.engine = Engine(self.config, self.cache, ctx)  # бизнес-логика\n\n        # Регистрация API-методов (вызываются: ctx.api.call(\"cookbook.метод\"))\n        ctx.api.register(\"cookbook\", \"ping\", self._api_ping)\n\n        # Подписка на события (публикуются: ctx.events.emit(\"cookbook.tick\"))\n        ctx.events.subscribe(\"cookbook.tick\", self._on_tick)\n\n    def start(self) -> None:\n        \"\"\"После setup() всех модулей. Здесь — фоновые потоки.\"\"\"\n        self._thread = threading.Thread(target=self._loop, daemon=True)\n        self._thread.start()\n\n    def stop(self) -> None:\n        \"\"\"При shutdown.\"\"\"\n        self._stop_event.set()\n        self._thread.join(timeout=5)\n\n    def health(self) -> dict:\n        \"\"\"Для --check и панели мониторинга.\"\"\"\n        return {\"ok\": True, \"module\": self.name, \"counter\": self.engine.count}\n```\n\n### 2. engine.py — бизнес-логика\n\nВынесена из module.py для разделения ответственности:\n- module.py — жизненный цикл, регистрация\n- engine.py — состояние, вычисления\n\n```python\nclass Engine:\n    def __init__(self, config, cache, ctx):\n        self.config = config  # ModuleConfig\n        self.cache = cache    # CacheService.Namespace\n        self.ctx = ctx        # CoreContext\n\n    @property\n    def count(self):\n        return self._count\n\n    @count.setter\n    def count(self, value):\n        self._count = value\n        self.cache.set(\"counter\", {\"count\": value})  # автосохранение\n```\n\n### 3. ui_tg.py — Telegram-интерфейс\n\n```python\n# Команды бота (строка = публичная, словарь = полный контроль)\nTG_COMMANDS = {\n    \"ping\": \"проверить живость\",\n    \"counter\": {\"desc\": \"счётчик\", \"right\": None, \"menu\": True},\n}\n\n# Кнопки модуля (колбэки: <модуль>:<action>)\nTG_CALLBACKS = {\n    \"increment\": {\"desc\": \"+1\", \"right\": None},\n}\n\n# Кнопки /start (главное меню)\nTG_MENU = [\n    {\"id\": \"cookbook_main\", \"title\": \"📖 Cookbook\", \"callback\": \"counter\"},\n]\n\ndef handle_command(ctx, config, command, text, inv=None):\n    # inv = {\"chat_id\": int, \"user_qid\": \"telegram:<id>\"}\n    if command == \"ping\":\n        return \"🟢 \" + ctx.api.call(\"cookbook.ping\")\n    return None\n\ndef handle_callback(ctx, config, action, args):\n    # args = {\"chat_id\", \"message_id\", \"message_text\", \"user_qid\"}\n    # Возврат: str (edit), (\"edit\", t), (\"send\", t), (\"toast\", t), None\n    if action == \"increment\":\n        return (\"toast\", \"+1\")\n    return None\n```\n\n### 4. ui_web.py — Web-интерфейс\n\n```python\n# REST API (путь обязан начинаться с /api/<mod>/)\nROUTES = [\n    (\"GET\", \"/api/cookbook/ping\", \"ping\", {\"right\": None}),\n    (\"POST\", \"/api/cookbook/reset\", \"reset\", {\"right\": None}),\n    (\"GET\", \"/api/cookbook/private\", \"private\", {\"right\": \"cookbook_private\"}),\n]\n\n# Карточка в кабинете (/api/services)\nSERVICE = {\n    \"key\": \"cookbook\", \"icon\": \"📖\", \"title\": \"Cookbook\",\n    \"right\": None, \"phase\": 1, \"desc\": \"Эталон модуля\",\n}\n\n# Панели мониторинга (/api/monitoring/layout)\nMONITOR_PANELS = [\n    {\"id\": \"cookbook_counter\", \"title\": \"Счётчик\",\n     \"api\": \"/api/cookbook/counter\", \"visibility\": \"public\", \"refresh_s\": 5},\n]\n\n# Навигация шапки SPA (/api/nav)\nNAV = [\n    {\"id\": \"cookbook\", \"title\": \"📖 Cookbook\", \"view\": \"cookbook\", \"icon\": \"📖\"},\n]\n\ndef handle_api(ctx, config, method, req):\n    # req = {\"http_method\", \"path\", \"query\", \"body\", \"files\",\n    #        \"user_qid\", \"session\", \"remote_ip\"}\n    from core.errors import UserError\n\n    if method == \"ping\":\n        return {\"ok\": True, \"pong\": True}  # → 200\n\n    if method == \"reset\":\n        body = req.get(\"body\") or {}\n        ctx.api.call(\"cookbook.reset\")\n        return {\"ok\": True}  # → 200\n\n    if method == \"private\":\n        # Право проверяется транспортом ДО handle_api\n        return {\"ok\": True, \"secret\": \"доступ разрешён\"}\n\n    raise UserError(\"неизвестный метод\")  # → 400\n```\n\n### 5. ui_ws.py — WebSocket + `web/` — кабинет модуля\n\n```python\nWS_PATH = \"/ws/cookbook/ticker\"  # обязан начинаться с /ws/\n\nКабинет — два файла, подхватываются транспортом сами (в `app.js` ядра\nничего дописывать не надо):\n- `web/view.html` — `<section id=\"view-<view>\">` с разметкой;\n- `web/view.js` — `loadFn` + саморгистрация:\n  `BotUI.registerService('<ключ SERVICE>', '<view>')` (карточка кабинета →\n  вьюха) и `BotUI.registerView('<view>', loadFn)`. Рендерер своей панели —\n  `BotUI.registerPanelRenderer('<id панели>', fn)` в том же файле.\n\nfrom botmod_transport_web.ws import make_base as _make_base\nBaseSocket = _make_base()\n\nclass WSSocket(BaseSocket):\n    RIGHT = \"cookbook_manage\"  # право для подключения\n\n    def on_authed(self, sess):\n        # self.mod_ctx — CoreContext\n        # self.ws_sess — данные сессии\n        self.write_message(json.dumps({\"type\": \"connected\"}))\n\n    def on_message(self, raw):\n        msg = json.loads(raw)\n        if msg[\"type\"] == \"ping\":\n            self.write_message(json.dumps({\"type\": \"pong\"}))\n\n    def on_close(self):\n        # Очистка\n        pass\n```\n\n### 6. manifest.yaml — манифест модуля\n\n```yaml\nname: cookbook                    # имя = имя папки (обязательно)\nversion: 1.0.0                    # семантическое.version\ncategory: dev                     # core | base | extra | dev\ndescription: Эталонный модуль...\n\n# API-методы для /api/registry\napi:\n  - cookbook.ping\n  - cookbook.counter\n\n# События\nevents_emitted:\n  - cookbook.tick\nevents_subscribed:\n  - cookbook.tick\n\n# TG-команды (для справки)\ntg_commands:\n  - ping\n  - counter\n\n# Web-роуты (для справки)\nweb_routes:\n  - GET /api/cookbook/ping\n  - GET /api/cookbook/private (right cookbook_private)\n\n# WS-пути (для справки)\nws_routes:\n  - /ws/cookbook/ticker\n\n# Подавление ложных срабатываний аудита\naudit_allow:\n  - NET_IMPORT\n\n# Права модуля (декларация)\nrights:\n  - name: cookbook_private\n    description: Доступ к приватному API\n  - name: cookbook_manage\n    description: Управление настройками\n\n# Файлы\nconfig_schema: config.schema.json\napi_methods:\n  - method: ping\n    args: \"{}\"\n    returns: \"str — 'pong'\"\n    desc: Проверка живости\nenv_example: .env.example\n```\n\n### 7. config.schema.json — схема конфигурации\n\n```json\n{\n  \"required\": [],\n  \"optional\": {\n    \"FACT_INTERVAL\": \"60\",\n    \"COUNTER_TICK_INTERVAL\": \"1\"\n  }\n}\n```\n\nСлои значений: `defaults` < `data/env/<name>.env` < `data/runtime_config.json`\n\n### 8. cache.py — кеш-неймспейсы\n\n```python\ndef counter_ns(ctx):\n    return ctx.cache.ns(\"cookbook\")  # data/cache/cookbook/*.json\n```\n\nКаждый модуль видит только свой namespace. Файловая блокировка flock.\n\n## Взаимодействие с другими модулями\n\n### Вызов API-метода\n```python\nresult = ctx.api.call(\"cookbook.ping\")\n```\n\n### Подписка на события\n```python\nctx.events.subscribe(\"cookbook.tick\", self._on_tick)\n```\n\n### Проверка права\n```python\nif ctx.rights.can(\"telegram:123\", \"cookbook_private\"):\n    ...\n```\n\n### Использование кеша\n```python\nns = ctx.cache.ns(\"cookbook\")\nns.set(\"key\", {\"data\": \"value\"})\ndata = ns.get(\"key\")\ndata = ns.get_ttl(\"key\", 60)  # с TTL\n```\n\n## Типы ошибок\n\n```python\nfrom core.errors import UserError, ConfigError, UpstreamError\n\n# UserError → 400 (плохой ввод, показать пользователю)\nraise UserError(\"неизвестный метод: %s\" % method)\n\n# ConfigError → fail-fast при setup (чинить руками)\nraise ConfigError(\"нет обязательного ключа X\")\n\n# UpstreamError → 500 (апстрим недоступен, можно ретраить)\nraise UpstreamError(\"ollama недоступен\")\n```\n\n## Проверка модуля\n\n```bash\n# Проверка без запуска\npython3 boot.py --check\n\n# Список модулей\npython3 boot.py --list-modules\n\n# Вызов API-метода\npython3 boot.py --call cookbook.ping\n\n# Runtime override\npython3 boot.py --set-override cookbook.FACT_INTERVAL 30\npython3 boot.py --clear-override cookbook.FACT_INTERVAL\n```\n\n## Создание нового модуля\n\n1. Скопировать `modules/cookbook/` в `modules/mynew/`\n2. Переименовать все файлы (cookbook → mynew)\n3. Обновить `manifest.yaml` (name, version, description)\n4. Обновить `module.py` (имя класса, API-методы)\n5. Обновить `ui_tg.py`, `ui_web.py`, `ui_ws.py`\n6. Включить через modsafe (web/API)\n7. `python3 boot.py --check`\n\n## Зависимости между модулями\n\nПоле `requires` в manifest.yaml:\n```yaml\nrequires:\n  - access      # загрузится после access\n  - models      # загрузится после models\n```\n\nЗависимости разрешаются до setup(). Нет зависимости — модуль пропускается.\nЦиклы — тоже пропуск (без падения бота).\n\n## Аудит модуля\n\nЯдро сканирует файлы модуля по эвристикам:\n- critical: eval, exec, compile, os.system, shell=True, pickle, marshal, ctypes\n- high: yaml.load, shutil.rmtree, хардкод секретов, os.exec/popen/fork\n- medium: динамический импорт, base64-декодирование, сетевые импорты\n- low: чтение окружения, запись файлов\n\nПодавление ложных срабатываний:\n```yaml\naudit_allow:\n  - NET_IMPORT   # сетевой импорт легитимен\n  - SUBPROC      # subprocess нужен для вызова внешних программ\n```\n\nХеши файлов: baseline при первом запуске, дальше — detect изменений (changed/new/missing).\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\"># cookbook </span><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">`1.0.0`</span><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\"> — эталонный модуль-справочник bot_modular</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Полный пример всех ключевых паттернов разработки модулей. Каждый файл модуля</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">прокомментирован и объясняет свой контракт с ядром.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Жизненный цикл модуля</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">loader.discover()          → сканирует modules/*/manifest.{yaml,json}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">modstate.is_enabled()      → default-off для новых (включить через modsafe)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">loader.load_module()       → импортирует module.py, создаёт Module()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  ├─ manifest.validate()   → обязательные поля: name, version, description</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  ├─ requires.resolve()    → ждёт зависимости (requires: [...])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  ├─ rights.register()     → регистрирует права из manifest.yaml</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  └─ module.setup(ctx)     # ← регистрация API, событий, UI</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">transport.start()          → после всех модулей</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  └─ module.start()        # ← фоновые потоки, подключения</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">module.stop()              # ← при shutdown</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Контракты вкладов</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">### 1. module.py — ядро модуля</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```python</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">class</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Module</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">BaseModule</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> setup</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, ctx) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Однажды при загрузке. НЕ поднимаем потоки.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.module_config(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># defaults &#x3C; data/env/*.env &#x3C; overrides</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.cache.ns(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)        </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># изолированный кеш</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Engine(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.cache, ctx)  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># бизнес-логика</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Регистрация API-методов (вызываются: ctx.api.call(\"cookbook.метод\"))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._api_ping)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Подписка на события (публикуются: ctx.events.emit(\"cookbook.tick\"))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.events.subscribe(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.tick\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._on_tick)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> start</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"После setup() всех модулей. Здесь — фоновые потоки.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._thread </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading.Thread(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">target</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._loop, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">daemon</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._thread.start()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> stop</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"При shutdown.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._stop_event.set()</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._thread.join(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">timeout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> health</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Для --check и панели мониторинга.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"module\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.name, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine.count}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">### 2. engine.py — бизнес-логика</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Вынесена из module.py для разделения ответственности:</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> module.py — жизненный цикл, регистрация</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> engine.py — состояние, вычисления</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```python</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">class</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Engine</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> __init__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, config, cache, ctx):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># ModuleConfig</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache    </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># CacheService.Namespace</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx        </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># CoreContext</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">    @</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">property</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> count</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._count</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">    @count.setter</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> count</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, value):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._count </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> value</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.cache.set(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: value})  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># автосохранение</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">### 3. ui_tg.py — Telegram-интерфейс</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```python</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Команды бота (строка = публичная, словарь = полный контроль)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TG_COMMANDS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"проверить живость\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"счётчик\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Кнопки модуля (колбэки: &#x3C;модуль>:&#x3C;action>)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TG_CALLBACKS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"increment\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"+1\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Кнопки /start (главное меню)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TG_MENU</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_main\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📖 Cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"callback\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> handle_command</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx, config, command, text, inv</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # inv = {\"chat_id\": int, \"user_qid\": \"telegram:&#x3C;id>\"}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> command </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"🟢 \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> handle_callback</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx, config, action, args):</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # args = {\"chat_id\", \"message_id\", \"message_text\", \"user_qid\"}</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # Возврат: str (edit), (\"edit\", t), (\"send\", t), (\"toast\", t), None</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> action </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"increment\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toast\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"+1\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">### 4. ui_web.py — Web-интерфейс</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```python</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># REST API (путь обязан начинаться с /api/&#x3C;mod>/)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ROUTES</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/reset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"reset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/private\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"private\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_private\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Карточка в кабинете (/api/services)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">SERVICE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"icon\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📖\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"phase\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Эталон модуля\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Панели мониторинга (/api/monitoring/layout)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">MONITOR_PANELS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Счётчик\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"api\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"visibility\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"public\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"refresh_s\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Навигация шапки SPA (/api/nav)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">NAV</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📖 Cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"view\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"icon\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📖\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> handle_api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx, config, method, req):</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # req = {\"http_method\", \"path\", \"query\", \"body\", \"files\",</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    #        \"user_qid\", \"session\", \"remote_ip\"}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.errors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"pong\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># → 200</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"reset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.reset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># → 200</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"private\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Право проверяется транспортом ДО handle_api</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"secret\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"доступ разрешён\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"неизвестный метод\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># → 400</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">### 5. ui_ws.py — WebSocket + </span><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">`web/`</span><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\"> — кабинет модуля</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```python</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">WS_PATH</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"/ws/cookbook/ticker\"</span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">  # обязан начинаться с /ws/</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Кабинет — два файла, подхватываются транспортом сами (в </span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">`app.js`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ядра</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ничего дописывать не надо):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\"> `web</span><span style=\"color:#D73A49;--shiki-light-font-style:italic;--shiki-dark:#F97583;--shiki-dark-font-style:italic\">/</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">view.html`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — </span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">`</span><span style=\"color:#D73A49;--shiki-light-font-style:italic;--shiki-dark:#F97583;--shiki-dark-font-style:italic\">&#x3C;</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">section </span><span style=\"color:#005CC5;--shiki-light-font-style:italic;--shiki-dark:#79B8FF;--shiki-dark-font-style:italic\">id</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">=</span><span style=\"color:#032F62;--shiki-light-font-style:italic;--shiki-dark:#9ECBFF;--shiki-dark-font-style:italic\">\"view-&#x3C;view>\"</span><span style=\"color:#D73A49;--shiki-light-font-style:italic;--shiki-dark:#F97583;--shiki-dark-font-style:italic\">></span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> с разметкой</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\"> `web</span><span style=\"color:#D73A49;--shiki-light-font-style:italic;--shiki-dark:#F97583;--shiki-dark-font-style:italic\">/</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">view.js`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — </span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">`loadFn`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> саморгистрация:</span></span>\n<span class=\"line\"><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">  `BotUI.registerService(</span><span style=\"color:#032F62;--shiki-light-font-style:italic;--shiki-dark:#9ECBFF;--shiki-dark-font-style:italic\">'&#x3C;ключ SERVICE>'</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">, </span><span style=\"color:#032F62;--shiki-light-font-style:italic;--shiki-dark:#9ECBFF;--shiki-dark-font-style:italic\">'&#x3C;view>'</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">)`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (карточка кабинета →</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  вьюха) и </span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">`BotUI.registerView(</span><span style=\"color:#032F62;--shiki-light-font-style:italic;--shiki-dark:#9ECBFF;--shiki-dark-font-style:italic\">'&#x3C;view>'</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">, loadFn)`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">. Рендерер своей панели —</span></span>\n<span class=\"line\"><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">  `BotUI.registerPanelRenderer(</span><span style=\"color:#032F62;--shiki-light-font-style:italic;--shiki-dark:#9ECBFF;--shiki-dark-font-style:italic\">'&#x3C;id панели>'</span><span style=\"color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic\">, fn)`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> в том же файле.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> botmod_transport_web.ws </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> make_base </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _make_base</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">BaseSocket </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _make_base()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">class</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> WSSocket</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">BaseSocket</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    RIGHT</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"cookbook_manage\"</span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">  # право для подключения</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> on_authed</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, sess):</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # self.mod_ctx — CoreContext</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # self.ws_sess — данные сессии</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.write_message(json.dumps({</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> on_message</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, raw):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json.loads(raw)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.write_message(json.dumps({</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"pong\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> on_close</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Очистка</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">### 6. manifest.yaml — манифест модуля</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```yaml</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cookbook</span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                    # имя = имя папки (обязательно)</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">version</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1.0.0</span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                    # семантическое.version</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">category</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">dev</span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                     # core | base | extra | dev</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Эталонный модуль...</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># API-методы для /api/registry</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cookbook.ping</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cookbook.counter</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># События</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">events_emitted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cookbook.tick</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">events_subscribed</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cookbook.tick</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># TG-команды (для справки)</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">tg_commands</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">ping</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">counter</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Web-роуты (для справки)</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">web_routes</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/cookbook/ping</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/cookbook/private (right cookbook_private)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># WS-пути (для справки)</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">ws_routes</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/ws/cookbook/ticker</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Подавление ложных срабатываний аудита</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">audit_allow</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">NET_IMPORT</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Права модуля (декларация)</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">rights</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cookbook_private</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Доступ к приватному API</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cookbook_manage</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Управление настройками</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Файлы</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">config_schema</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">config.schema.json</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">api_methods</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">ping</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"str — 'pong'\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Проверка живости</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">env_example</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.env.example</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">### 7. config.schema.json — схема конфигурации</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```json</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">{</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"required\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: [],</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"optional\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"FACT_INTERVAL\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"60\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"COUNTER_TICK_INTERVAL\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Слои значений: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`defaults`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> &#x3C; </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`data/env/&#x3C;name>.env`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> &#x3C; </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`data/runtime_config.json`</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">### 8. cache.py — кеш-неймспейсы</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```python</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> counter_ns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.cache.ns(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># data/cache/cookbook/*.json</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Каждый модуль видит только свой namespace. Файловая блокировка flock.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Взаимодействие с другими модулями</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">### Вызов API-метода</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```python</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">result </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">### Подписка на события</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```python</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ctx.events.subscribe(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.tick\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._on_tick)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">### Проверка права</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```python</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.rights.can(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"telegram:123\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_private\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    ...</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">### Использование кеша</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```python</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ns </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.cache.ns(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ns.set(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"data\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"value\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">})</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ns.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ns.get_ttl(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">60</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># с TTL</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Типы ошибок</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```python</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.errors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError, ConfigError, UpstreamError</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># UserError → 400 (плохой ввод, показать пользователю)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"неизвестный метод: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># ConfigError → fail-fast при setup (чинить руками)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ConfigError(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"нет обязательного ключа X\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># UpstreamError → 500 (апстрим недоступен, можно ретраить)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UpstreamError(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ollama недоступен\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Проверка модуля</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```bash</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"># Проверка без запуска</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">python3 boot.py --check</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"># Список модулей</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">python3 boot.py --list-modules</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"># Вызов API-метода</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">python3 boot.py --call cookbook.ping</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"># Runtime override</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">python3 boot.py --set-override cookbook.FACT_INTERVAL 30</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">python3 boot.py --clear-override cookbook.FACT_INTERVAL</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Создание нового модуля</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">1.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Скопировать </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`modules/cookbook/`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> в </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`modules/mynew/`</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">2.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Переименовать все файлы (cookbook → mynew)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">3.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Обновить </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`manifest.yaml`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (name, version, description)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">4.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Обновить </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`module.py`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (имя класса, API-методы)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">5.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Обновить </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`ui_tg.py`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`ui_web.py`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`ui_ws.py`</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">6.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Включить через modsafe (web/API)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">7.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `python3 boot.py --check`</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Зависимости между модулями</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Поле </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`requires`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> в manifest.yaml:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```yaml</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">requires</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">access</span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">      # загрузится после access</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">models</span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">      # загрузится после models</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Зависимости разрешаются до setup(). Нет зависимости — модуль пропускается.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Циклы — тоже пропуск (без падения бота).</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Аудит модуля</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Ядро сканирует файлы модуля по эвристикам:</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> critical: eval, exec, compile, os.system, shell=True, pickle, marshal, ctypes</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> high: yaml.load, shutil.rmtree, хардкод секретов, os.exec/popen/fork</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> medium: динамический импорт, base64-декодирование, сетевые импорты</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> low: чтение окружения, запись файлов</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Подавление ложных срабатываний:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```yaml</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">audit_allow</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">NET_IMPORT</span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">   # сетевой импорт легитимен</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">SUBPROC</span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">      # subprocess нужен для вызова внешних программ</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">```</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Хеши файлов: baseline при первом запуске, дальше — detect изменений (changed/new/missing).</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "cache.py",
          "size": 721,
          "content": "\"\"\"cookbook.cache — кеш-неймспейсы модуля.\n\nКаждый модуль видит только свой namespace: data/cache/cookbook/*.json.\nКонкурентный доступ сериализуется flock.\n\nПаттерн:\n  def my_ns(ctx):\n      return ctx.cache.ns(\"cookbook\")\n\nИспользование:\n  ns = my_ns(ctx)\n  ns.set(\"key\", {\"data\": \"value\"})   # атомарная запись (tmp + rename)\n  data = ns.get(\"key\")               # блокировка SH\n  data = ns.get_ttl(\"key\", 60)       # с TTL по mtime\n\"\"\"\n\n\ndef counter_ns(ctx):\n    \"\"\"Кеш для счётчика (управляется Engine, но namespace тут).\"\"\"\n    return ctx.cache.ns(\"cookbook\")\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"cookbook.cache — кеш-неймспейсы модуля.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Каждый модуль видит только свой namespace: data/cache/cookbook/*.json.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Конкурентный доступ сериализуется flock.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Паттерн:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  def my_ns(ctx):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      return ctx.cache.ns(\"cookbook\")</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Использование:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  ns = my_ns(ctx)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  ns.set(\"key\", {\"data\": \"value\"})   # атомарная запись (tmp + rename)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  data = ns.get(\"key\")               # блокировка SH</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  data = ns.get_ttl(\"key\", 60)       # с TTL по mtime</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> counter_ns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Кеш для счётчика (управляется Engine, но namespace тут).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.cache.ns(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "config.schema.json",
          "size": 182,
          "content": "{\n  \"required\": [],\n  \"optional\": {\n    \"FACT_INTERVAL\": \"60\",\n    \"COUNTER_TICK_INTERVAL\": \"1\",\n    \"DEFAULT_FACT\": \"bot_modular — модульный бот-комбайн\"\n  }\n}\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">{</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"required\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: [],</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"optional\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"FACT_INTERVAL\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"60\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"COUNTER_TICK_INTERVAL\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"DEFAULT_FACT\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"bot_modular — модульный бот-комбайн\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "engine.py",
          "size": 5344,
          "content": "\"\"\"cookbook.engine — бизнес-логика модуля.\n\nВынесена из module.py для разделения ответственности:\n  module.py  — жизненный цикл, регистрация API/событий\n  engine.py  — состояние, вычисления, взаимодействие с внешними системами\n\nДвижок получает config, cache и ctx в конструкторе.\nСостояние хранится в cache (персистентно) и в памяти (для скорости).\n\"\"\"\nfrom __future__ import annotations\n\nimport random\nimport time\n\n\nclass Engine:\n    \"\"\"Состояние и бизнес-логика модуля cookbook.\"\"\"\n\n    # --- Факты — эталонный список (замените на свои) ---\n    FACTS = [\n        \"bot_modular использует PyYAML для манифестов модулей\",\n        \"Каждый модуль — отдельная папка с manifest.yaml и module.py\",\n        \"Модули общаются только через ctx — прямые импорты запрещены\",\n        \"Права проверяются через ctx.rights.can(qualified_id, right)\",\n        \"Кеш модулей изолирован: data/cache/<module>/<key>.json\",\n        \"Runtime overrides пишутся в data/runtime_config.json\",\n        \"Транспорт Telegram поддерживает http и kurigram с failover\",\n        \"Веб-панель на Tornado: SPA, WebSocket, авто-TLS\",\n        \"Статический аудит ловит eval, exec, pickle, обфускацию\",\n        \"Новые модули по умолчанию выключены (modsafe + default-off)\",\n    ]\n\n    def __init__(self, config, cache, ctx):\n        \"\"\"Инициализация движка.\n\n        Args:\n            config: ModuleConfig — конфигурация модуля (defaults < .env < overrides)\n            cache: CacheService.Namespace — кеш-неймспейс модуля\n            ctx: CoreContext — контекст ядра (api, events, rights, cache)\n        \"\"\"\n        self.config = config\n        self.cache = cache\n        self.ctx = ctx\n\n        # Счётчик — из кеша (персистентно) или 0\n        # Важно: started_at ДО count, т.к. setter count читает started_at\n        saved = cache.get(\"counter\")\n        self.started_at = saved[\"started_at\"] if saved else time.time()\n        self._count = saved[\"count\"] if saved else 0\n\n    @property\n    def count(self) -> int:\n        \"\"\"Текущее значение счётчика.\"\"\"\n        return self._count\n\n    @count.setter\n    def count(self, value: int) -> None:\n        self._count = value\n        # Автосохранение в кеш при каждом изменении.\n        self.cache.set(\"counter\", {\n            \"count\": value,\n            \"started_at\": self.started_at,\n        })\n\n    def get_counter(self) -> dict:\n        \"\"\"Вернуть текущий счётчик.\"\"\"\n        return {\n            \"count\": self.count,\n            \"started_at\": self.started_at,\n        }\n\n    def increment_counter(self) -> None:\n        \"\"\"Увеличить счётчик на 1.\"\"\"\n        self.count += 1\n\n    def reset_counter(self) -> None:\n        \"\"\"Сбросить счётчик.\"\"\"\n        self.count = 0\n\n    def get_random_fact(self) -> str:\n        \"\"\"Вернуть случайный факт из списка.\n\n        Если FACTS пуст — возвращаем DEFAULT_FACT из конфига.\n        \"\"\"\n        if self.FACTS:\n            return random.choice(self.FACTS)\n        return self.config.get(\"DEFAULT_FACT\", \"bot_modular\")\n\n    def get_settings(self) -> dict:\n        \"\"\"Вернуть текущие настройки.\n\n        Включает: defaults, .env, runtime overrides.\n        \"\"\"\n        return {\n            \"defaults\": dict(self.config.defaults),\n            \"env_values\": {k: v for k, v in self.config.values.items()\n                           if k in self.config.defaults},\n            \"overrides\": self.config.override_keys,\n        }\n\n    def set_setting(self, key: str, value: str) -> None:\n        \"\"\"Записать runtime override для ключа.\n\n        Ключ валидируется по схеме модуля — неизвестный ключ вызовет\n        ConfigError на уровне module.py (через ctx.runtime.set_override).\n        \"\"\"\n        from core.errors import ConfigError\n\n        allowed = set(self.config.required) | set(self.config.defaults)\n        if key not in allowed:\n            raise ConfigError(\n                \"cookbook: ключ %s не входит в схему\" % key\n            )\n        self.ctx.runtime.set_override(\"cookbook\", key, value, allowed=allowed)\n\n    def on_tick(self, payload: dict | None = None) -> None:\n        \"\"\"Обработка события cookbook.tick.\n\n        Args:\n            payload: произвольные данные от издателя\n        \"\"\"\n        # Можно логировать, обновлять состояние, отправлять уведомления.\n        if payload:\n            pass  # обработка payload\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"cookbook.engine — бизнес-логика модуля.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Вынесена из module.py для разделения ответственности:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  module.py  — жизненный цикл, регистрация API/событий</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  engine.py  — состояние, вычисления, взаимодействие с внешними системами</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Движок получает config, cache и ctx в конструкторе.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Состояние хранится в cache (персистентно) и в памяти (для скорости).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> __future__</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> annotations</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> random</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> time</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">class</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Engine</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Состояние и бизнес-логика модуля cookbook.\"\"\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # --- Факты — эталонный список (замените на свои) ---</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    FACTS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"bot_modular использует PyYAML для манифестов модулей\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"Каждый модуль — отдельная папка с manifest.yaml и module.py\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"Модули общаются только через ctx — прямые импорты запрещены\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"Права проверяются через ctx.rights.can(qualified_id, right)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"Кеш модулей изолирован: data/cache/&#x3C;module>/&#x3C;key>.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"Runtime overrides пишутся в data/runtime_config.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"Транспорт Telegram поддерживает http и kurigram с failover\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"Веб-панель на Tornado: SPA, WebSocket, авто-TLS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"Статический аудит ловит eval, exec, pickle, обфускацию\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"Новые модули по умолчанию выключены (modsafe + default-off)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    ]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> __init__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, config, cache, ctx):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Инициализация движка.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Args:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            config: ModuleConfig — конфигурация модуля (defaults &#x3C; .env &#x3C; overrides)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            cache: CacheService.Namespace — кеш-неймспейс модуля</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            ctx: CoreContext — контекст ядра (api, events, rights, cache)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Счётчик — из кеша (персистентно) или 0</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Важно: started_at ДО count, т.к. setter count читает started_at</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        saved </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.started_at </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> saved[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"started_at\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> saved </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> time.time()</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._count </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> saved[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> saved </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">    @</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">property</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> count</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Текущее значение счётчика.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._count</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">    @count.setter</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> count</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, value: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._count </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> value</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Автосохранение в кеш при каждом изменении.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.cache.set(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: value,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"started_at\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.started_at,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        })</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> get_counter</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Вернуть текущий счётчик.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.count,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"started_at\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.started_at,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> increment_counter</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Увеличить счётчик на 1.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.count </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> reset_counter</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Сбросить счётчик.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.count </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> get_random_fact</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Вернуть случайный факт из списка.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Если FACTS пуст — возвращаем DEFAULT_FACT из конфига.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">FACTS</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> random.choice(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">FACTS</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"DEFAULT_FACT\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"bot_modular\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> get_settings</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Вернуть текущие настройки.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Включает: defaults, .env, runtime overrides.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"defaults\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.defaults),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"env_values\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {k: v </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k, v </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.values.items()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                           if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.defaults},</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"overrides\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.override_keys,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> set_setting</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, key: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, value: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Записать runtime override для ключа.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Ключ валидируется по схеме модуля — неизвестный ключ вызовет</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        ConfigError на уровне module.py (через ctx.runtime.set_override).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.errors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ConfigError</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        allowed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.required) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.defaults)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> allowed:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ConfigError(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"cookbook: ключ </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> не входит в схему\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            )</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx.runtime.set_override(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, key, value, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">allowed</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">allowed)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> on_tick</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, payload: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> |</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Обработка события cookbook.tick.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Args:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            payload: произвольные данные от издателя</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Можно логировать, обновлять состояние, отправлять уведомления.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> payload:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">  # обработка payload</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "manifest.yaml",
          "size": 1991,
          "content": "name: cookbook\nversion: 1.0.0\ncategory: dev\ndescription: Эталонный модуль-справочник — показывает все ключевые паттерны разработки модулей bot_modular.\napi:\n  - cookbook.ping\n  - cookbook.counter\n  - cookbook.reset\n  - cookbook.fact\nevents_emitted:\n  - cookbook.tick\nevents_subscribed:\n  - cookbook.tick\ntg_commands:\n  - ping\n  - counter\n  - reset\n  - fact\nweb_routes:\n  - GET /api/cookbook/ping\n  - GET /api/cookbook/counter\n  - POST /api/cookbook/reset\n  - GET /api/cookbook/fact\n  - GET /api/cookbook/settings\n  - POST /api/cookbook/settings\n  - GET /api/cookbook/health\n  - GET /api/cookbook/private (right cookbook_private)\nws_routes:\n  - /ws/cookbook/ticker\naudit_allow:\n  - NET_IMPORT\n  - SUBPROC\n  - FILE_WRITE\n\nrights:\n  - name: cookbook_private\n    description: Доступ к приватному API модуля cookbook\n  - name: cookbook_manage\n    description: Управление настройками модуля cookbook\n\nconfig_schema: config.schema.json\napi_methods:\n  - method: ping\n    args: \"{}\"\n    returns: \"str — 'pong'\"\n    desc: Простейший API-метод без аргументов\n  - method: counter\n    args: \"{}\"\n    returns: \"dict — {count, started_at}\"\n    desc: Получить текущий счётчик\n  - method: reset\n    args: \"{}\"\n    returns: \"dict — {ok: true, reset_to: 0}\"\n    desc: Сбросить счётчик\n  - method: fact\n    args: \"{}\"\n    returns: \"str — случайный факт\"\n    desc: Вернуть случайный факт из списка\n  - method: settings_get\n    args: \"{}\"\n    returns: \"dict — текущие настройки\"\n    desc: Прочитать настройки модуля\n  - method: settings_set\n    args: '{\"key\": \"string\", \"value\": \"string\"}'\n    returns: \"dict — {ok: true}\"\n    desc: Записать настройку (валидация по схеме)\nenv_example: .env.example\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cookbook</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">version</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1.0.0</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">category</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">dev</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Эталонный модуль-справочник — показывает все ключевые паттерны разработки модулей bot_modular.</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cookbook.ping</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cookbook.counter</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cookbook.reset</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cookbook.fact</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">events_emitted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cookbook.tick</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">events_subscribed</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cookbook.tick</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">tg_commands</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">ping</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">counter</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">reset</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">fact</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">web_routes</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/cookbook/ping</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/cookbook/counter</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">POST /api/cookbook/reset</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/cookbook/fact</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/cookbook/settings</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">POST /api/cookbook/settings</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/cookbook/health</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/cookbook/private (right cookbook_private)</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">ws_routes</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/ws/cookbook/ticker</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">audit_allow</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">NET_IMPORT</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">SUBPROC</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">FILE_WRITE</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">rights</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cookbook_private</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Доступ к приватному API модуля cookbook</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cookbook_manage</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Управление настройками модуля cookbook</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">config_schema</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">config.schema.json</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">api_methods</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">ping</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"str — 'pong'\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Простейший API-метод без аргументов</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">counter</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"dict — {count, started_at}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Получить текущий счётчик</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">reset</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"dict — {ok: true, reset_to: 0}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Сбросить счётчик</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">fact</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"str — случайный факт\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Вернуть случайный факт из списка</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">settings_get</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"dict — текущие настройки\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Прочитать настройки модуля</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">settings_set</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'{\"key\": \"string\", \"value\": \"string\"}'</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"dict — {ok: true}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Записать настройку (валидация по схеме)</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">env_example</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.env.example</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "module.py",
          "size": 11071,
          "content": "\"\"\"cookbook.Module — эталонный модуль-справочник.\n\nДемонстрирует все ключевые паттерны разработки модулей bot_modular:\n  1. Жизненный цикл: setup → start → stop → health\n  2. Регистрация API-методов (ctx.api.register)\n  3. Подписка на события (ctx.events.subscribe)\n  4. Конфигурация (ctx.module_config)\n  5. Кеш-неймспейс (ctx.cache.ns)\n  6. Runtime overrides (ctx.runtime)\n  7. Взаимодействие с другими модулями (ctx.api.call)\n  8. Telegram UI (TG_COMMANDS + handle_command)\n  9. Web UI (ROUTES + handle_api)\n  10. WebSocket UI (WS_PATH + WSSocket)\n  11. Callback-кнопки (TG_CALLBACKS + handle_callback)\n  12. Меню /start (TG_MENU)\n  13. Сервис кабинета (SERVICE)\n  14. Панели мониторинга (MONITOR_PANELS)\n  15. Навигация SPA (NAV)\n  16. Права (manifest rights + ctx.rights.can)\n  17. Ошибки (UserError, BotError)\n  18. Фоновый цикл (start/stop с потоком)\n\nСтруктура файлов модуля:\n  module.py      — ядро: setup/start/stop/health, регистрация API\n  engine.py      — бизнес-логика (вынесена из module.py)\n  ui_tg.py       — Telegram-интерфейс (команды, callbacks, меню)\n  ui_web.py      — Web-интерфейс (ROUTES + handle_api)\n  ui_ws.py       — WebSocket (WS_PATH + WSSocket)\n  cache.py       — кеш-неймспейсы модуля\n  .env.example   — пример конфига\n  config.schema.json — схема конфигурации\n  manifest.yaml  — манифест модуля\n\"\"\"\nfrom __future__ import annotations\n\nimport logging\n\nfrom core.base_module import BaseModule\nfrom core.errors import ConfigError\n\nlogger = logging.getLogger(__name__)\n\n\nclass Module(BaseModule):\n    name = \"cookbook\"\n    version = \"1.0.0\"\n\n    def setup(self, ctx) -> None:\n        \"\"\"Настраиваем модуль: конфиг, кеш, API, события, права.\n\n        setup() вызывается один раз при загрузке модуля загрузчиком (loader).\n        Здесь НЕ поднимаем фоновые потоки — только регистрация.\n        Ошибки конфигурации — бросаем ConfigError (бот пропустит модуль).\n        \"\"\"\n        # --- 3. Конфигурация модуля ---\n        # ctx.module_config() собирает: defaults < .env < runtime overrides.\n        # Валидация по config.schema.json: обязательные + опциональные ключи.\n        self.config = ctx.module_config(\"cookbook\")\n\n        # --- 4. Кеш-неймспейс ---\n        # Каждый модуль видит только свой namespace: data/cache/cookbook/*.json.\n        # Конкурентный доступ сериализуется flock.\n        self.cache = self._setup_cache(ctx)\n\n        # --- 5. Инициализация бизнес-логики ---\n        # Движок содержит состояние и методы модуля.\n        self.engine = None\n        try:\n            from . import engine as cookbook_engine\n            self.engine = cookbook_engine.Engine(self.config, self.cache, ctx)\n        except ImportError as e:\n            raise ConfigError(\n                \"cookbook: движок требует зависимости: %s\" % e\n            ) from None\n\n        # --- 1. Регистрация API-методов ---\n        # Другие модули вызывают: ctx.api.call(\"cookbook.метод\", ...)\n        # Каждый метод документируется в __doc__ (для /api/registry).\n        ctx.api.register(\"cookbook\", \"ping\", self._api_ping)\n        ctx.api.register(\"cookbook\", \"counter\", self._api_counter)\n        ctx.api.register(\"cookbook\", \"reset\", self._api_reset)\n        ctx.api.register(\"cookbook\", \"fact\", self._api_fact)\n        ctx.api.register(\"cookbook\", \"settings_get\", self._api_settings_get)\n        ctx.api.register(\"cookbook\", \"settings_set\", self._api_settings_set)\n\n        # --- 6. Подписка на события ---\n        # Другие модули публикуют: ctx.events.emit(\"cookbook.tick\", payload)\n        # Упавший подписчик не роняет остальных (dead-letter queue).\n        ctx.events.subscribe(\"cookbook.tick\", self._on_tick)\n\n        # --- 7. Хук on_ready транспорта ---\n        # Вызывается при активации транспорта (первый poll/fetch).\n        # Можно отправить стартовое уведомление.\n        try:\n            ctx.api.call(\"transport_telegram.on_ready\", self._on_tg_ready)\n        except Exception:\n            pass  # транспорта ещё нет — не критично\n\n    def start(self) -> None:\n        \"\"\"Поднимаем фоновую активность (потоки, tasks).\n\n        start() вызывается после setup() всех модулей.\n        Здесь запускаем фоновые циклы, подключения, слушатели.\n        \"\"\"\n        if self.engine is None:\n            return\n\n        # --- 18. Фоновый цикл ---\n        # Запускаем поток для периодического тика счётчика.\n        import threading\n        self._stop_event = threading.Event()\n        self._thread = threading.Thread(\n            target=self._ticker_loop,\n            name=\"cookbook:ticker\",\n            daemon=True,\n        )\n        self._thread.start()\n\n    def stop(self) -> None:\n        \"\"\"Останавливаем фоновую активность.\"\"\"\n        if hasattr(self, \"_stop_event\"):\n            self._stop_event.set()\n        if hasattr(self, \"_thread\") and self._thread is not None:\n            self._thread.join(timeout=5)\n\n    def health(self) -> dict:\n        \"\"\"Состояние модуля для мониторинга.\n\n        Возвращается при --check и читается панелью мониторинга.\n        \"\"\"\n        return {\n            \"ok\": True,\n            \"module\": self.name,\n            \"api\": [\n                \"cookbook.ping\",\n                \"cookbook.counter\",\n                \"cookbook.reset\",\n                \"cookbook.fact\",\n                \"cookbook.settings_get\",\n                \"cookbook.settings_set\",\n            ],\n            \"counter\": self.engine.count if self.engine else 0,\n            \"thread_alive\": getattr(self, \"_thread\", None) is not None\n                             and self._thread.is_alive(),\n        }\n\n    # --- API-методы (вызываются через ctx.api.call) ---\n\n    def _api_ping(self) -> str:\n        \"\"\"cookbook.ping: проверка живости модуля.\"\"\"\n        return \"pong\"\n\n    def _api_counter(self) -> dict:\n        \"\"\"cookbook.counter: текущий счётчик.\"\"\"\n        if self.engine is None:\n            from core.errors import UserError\n            raise UserError(\"модуль не инициализирован\")\n        return self.engine.get_counter()\n\n    def _api_reset(self) -> dict:\n        \"\"\"cookbook.reset: сбросить счётчик.\"\"\"\n        if self.engine is None:\n            from core.errors import UserError\n            raise UserError(\"модуль не инициализирован\")\n        self.engine.reset_counter()\n        return {\"ok\": True, \"reset_to\": 0}\n\n    def _api_fact(self) -> str:\n        \"\"\"cookbook.fact: случайный факт из списка.\"\"\"\n        if self.engine is None:\n            from core.errors import UserError\n            raise UserError(\"модуль не инициализирован\")\n        return self.engine.get_random_fact()\n\n    def _api_settings_get(self) -> dict:\n        \"\"\"cookbook.settings_get: прочитать настройки.\"\"\"\n        if self.engine is None:\n            from core.errors import UserError\n            raise UserError(\"модуль не инициализирован\")\n        return self.engine.get_settings()\n\n    def _api_settings_set(self, key: str, value: str) -> dict:\n        \"\"\"cookbook.settings_set: записать настройку (runtime override).\n\n        Аргументы передаются как именованные параметры lambda.\n        Валидация ключа — по схеме модуля.\n        \"\"\"\n        if self.engine is None:\n            from core.errors import UserError\n            raise UserError(\"модуль не инициализирован\")\n        self.engine.set_setting(key, value)\n        return {\"ok\": True, \"key\": key}\n\n    # --- Обработчики событий ---\n\n    def _on_tick(self, payload):\n        \"\"\"Обработка события cookbook.tick.\n\n        payload — произвольные данные от издателя.\n        Возвращаемое значение игнорируется (но может быть использовано).\n        \"\"\"\n        if self.engine is None:\n            return None\n        self.engine.on_tick(payload)\n        return \"tick handled\"\n\n    # --- Фоновый цикл ---\n\n    def _ticker_loop(self):\n        \"\"\"Фоновый поток: периодический тик счётчика + публикация события.\"\"\"\n        import time\n        import logging\n\n        log = logging.getLogger(\"botmod.cookbook.ticker\")\n        while not self._stop_event.is_set():\n            try:\n                if self.engine is not None:\n                    self.engine.increment_counter()\n                    # Публикуем событие для всех подписчиков.\n                    self.ctx.events.emit(\"cookbook.tick\", {\n                        \"source\": \"ticker\",\n                        \"count\": self.engine.count,\n                    })\n            except Exception as e:\n                log.error(\"ticker error: %s\", e)\n            # Спим по интервалу из конфига (по умолчанию 1 сек).\n            interval = float(self.config.get(\"COUNTER_TICK_INTERVAL\", \"1\") or 1)\n            self._stop_event.wait(timeout=interval)\n\n    # --- Хуки транспорта ---\n\n    def _on_tg_ready(self, transport_name: str):\n        \"\"\"Вызывается при активации Telegram-транспорта.\"\"\"\n        # Можно отправить стартовое сообщение в админ-чат.\n        # Пример: ctx.api.call(\"transport_telegram.send\", chat_id, \"cookbook ready\")\n        pass\n\n    # --- Вспомогательные методы ---\n\n    def _setup_cache(self, ctx):\n        \"\"\"Инициализация кеш-неймспейса модуля.\"\"\"\n        return ctx.cache.ns(\"cookbook\")\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"cookbook.Module — эталонный модуль-справочник.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Демонстрирует все ключевые паттерны разработки модулей bot_modular:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  1. Жизненный цикл: setup → start → stop → health</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  2. Регистрация API-методов (ctx.api.register)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  3. Подписка на события (ctx.events.subscribe)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  4. Конфигурация (ctx.module_config)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  5. Кеш-неймспейс (ctx.cache.ns)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  6. Runtime overrides (ctx.runtime)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  7. Взаимодействие с другими модулями (ctx.api.call)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  8. Telegram UI (TG_COMMANDS + handle_command)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  9. Web UI (ROUTES + handle_api)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  10. WebSocket UI (WS_PATH + WSSocket)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  11. Callback-кнопки (TG_CALLBACKS + handle_callback)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  12. Меню /start (TG_MENU)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  13. Сервис кабинета (SERVICE)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  14. Панели мониторинга (MONITOR_PANELS)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  15. Навигация SPA (NAV)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  16. Права (manifest rights + ctx.rights.can)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  17. Ошибки (UserError, BotError)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  18. Фоновый цикл (start/stop с потоком)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Структура файлов модуля:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  module.py      — ядро: setup/start/stop/health, регистрация API</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  engine.py      — бизнес-логика (вынесена из module.py)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  ui_tg.py       — Telegram-интерфейс (команды, callbacks, меню)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  ui_web.py      — Web-интерфейс (ROUTES + handle_api)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  ui_ws.py       — WebSocket (WS_PATH + WSSocket)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  cache.py       — кеш-неймспейсы модуля</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  .env.example   — пример конфига</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  config.schema.json — схема конфигурации</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  manifest.yaml  — манифест модуля</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> __future__</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> annotations</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.base_module </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> BaseModule</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.errors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ConfigError</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">logger </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging.getLogger(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__name__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">class</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Module</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">BaseModule</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"cookbook\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    version </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"1.0.0\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> setup</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, ctx) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Настраиваем модуль: конфиг, кеш, API, события, права.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        setup() вызывается один раз при загрузке модуля загрузчиком (loader).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Здесь НЕ поднимаем фоновые потоки — только регистрация.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Ошибки конфигурации — бросаем ConfigError (бот пропустит модуль).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # --- 3. Конфигурация модуля ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # ctx.module_config() собирает: defaults &#x3C; .env &#x3C; runtime overrides.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Валидация по config.schema.json: обязательные + опциональные ключи.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.module_config(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # --- 4. Кеш-неймспейс ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Каждый модуль видит только свой namespace: data/cache/cookbook/*.json.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Конкурентный доступ сериализуется flock.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._setup_cache(ctx)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # --- 5. Инициализация бизнес-логики ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Движок содержит состояние и методы модуля.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> engine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cookbook_engine</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cookbook_engine.Engine(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.cache, ctx)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ImportError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ConfigError(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"cookbook: движок требует зависимости: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # --- 1. Регистрация API-методов ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Другие модули вызывают: ctx.api.call(\"cookbook.метод\", ...)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Каждый метод документируется в __doc__ (для /api/registry).</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._api_ping)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._api_counter)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"reset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._api_reset)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"fact\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._api_fact)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"settings_get\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._api_settings_get)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"settings_set\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._api_settings_set)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # --- 6. Подписка на события ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Другие модули публикуют: ctx.events.emit(\"cookbook.tick\", payload)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Упавший подписчик не роняет остальных (dead-letter queue).</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.events.subscribe(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.tick\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._on_tick)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # --- 7. Хук on_ready транспорта ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Вызывается при активации транспорта (первый poll/fetch).</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Можно отправить стартовое уведомление.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"transport_telegram.on_ready\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._on_tg_ready)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">  # транспорта ещё нет — не критично</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> start</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Поднимаем фоновую активность (потоки, tasks).</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        start() вызывается после setup() всех модулей.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Здесь запускаем фоновые циклы, подключения, слушатели.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # --- 18. Фоновый цикл ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Запускаем поток для периодического тика счётчика.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._stop_event </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading.Event()</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._thread </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading.Thread(</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            target</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._ticker_loop,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            name</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook:ticker\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            daemon</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        )</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._thread.start()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> stop</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Останавливаем фоновую активность.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> hasattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"_stop_event\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._stop_event.set()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> hasattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"_thread\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._thread </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._thread.join(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">timeout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> health</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Состояние модуля для мониторинга.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Возвращается при --check и читается панелью мониторинга.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"module\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.name,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"api\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: [</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"cookbook.ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"cookbook.counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"cookbook.reset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"cookbook.fact\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"cookbook.settings_get\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"cookbook.settings_set\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine.count </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"thread_alive\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"_thread\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                             and</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._thread.is_alive(),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # --- API-методы (вызываются через ctx.api.call) ---</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _api_ping</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"cookbook.ping: проверка живости модуля.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"pong\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _api_counter</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"cookbook.counter: текущий счётчик.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.errors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"модуль не инициализирован\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine.get_counter()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _api_reset</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"cookbook.reset: сбросить счётчик.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.errors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"модуль не инициализирован\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine.reset_counter()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"reset_to\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _api_fact</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"cookbook.fact: случайный факт из списка.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.errors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"модуль не инициализирован\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine.get_random_fact()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _api_settings_get</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"cookbook.settings_get: прочитать настройки.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.errors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"модуль не инициализирован\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine.get_settings()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _api_settings_set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, key: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, value: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"cookbook.settings_set: записать настройку (runtime override).</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Аргументы передаются как именованные параметры lambda.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Валидация ключа — по схеме модуля.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.errors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"модуль не инициализирован\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine.set_setting(key, value)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: key}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # --- Обработчики событий ---</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _on_tick</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, payload):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Обработка события cookbook.tick.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        payload — произвольные данные от издателя.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Возвращаемое значение игнорируется (но может быть использовано).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine.on_tick(payload)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"tick handled\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # --- Фоновый цикл ---</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _ticker_loop</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Фоновый поток: периодический тик счётчика + публикация события.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> time</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        log </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging.getLogger(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"botmod.cookbook.ticker\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        while</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._stop_event.is_set():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine.increment_counter()</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                    # Публикуем событие для всех подписчиков.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx.events.emit(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.tick\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                        \"source\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ticker\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                        \"count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.engine.count,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    })</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                log.error(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ticker error: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, e)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Спим по интервалу из конфига (по умолчанию 1 сек).</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            interval </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> float</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"COUNTER_TICK_INTERVAL\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._stop_event.wait(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">timeout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">interval)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # --- Хуки транспорта ---</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _on_tg_ready</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, transport_name: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Вызывается при активации Telegram-транспорта.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Можно отправить стартовое сообщение в админ-чат.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Пример: ctx.api.call(\"transport_telegram.send\", chat_id, \"cookbook ready\")</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        pass</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # --- Вспомогательные методы ---</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _setup_cache</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, ctx):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Инициализация кеш-неймспейса модуля.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.cache.ns(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "requirements.txt",
          "size": 232,
          "content": "# cookbook — модульные зависимости\n# Общие зависимости — в корневом requirements.txt\n# Свои пакеты — в modules/<name>/requirements.txt\n# Пример:\n#   requests>=2.28\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span># cookbook — модульные зависимости</span></span>\n<span class=\"line\"><span># Общие зависимости — в корневом requirements.txt</span></span>\n<span class=\"line\"><span># Свои пакеты — в modules/&#x3C;name>/requirements.txt</span></span>\n<span class=\"line\"><span># Пример:</span></span>\n<span class=\"line\"><span>#   requests>=2.28</span></span>\n<span class=\"line\"><span></span></span></code></pre>"
        },
        {
          "path": "ui_tg.py",
          "size": 7987,
          "content": "\"\"\"cookbook.ui_tg — Telegram-интерфейс модуля.\n\nДемонстрирует все паттерны TG-интерфейса:\n  1. TG_COMMANDS — команды бота (простая строка или словарь с meta)\n  2. TG_CALLBACKS — кнопки модуля (колбэки с паттерном <модуль>:<action>)\n  3. TG_MENU — кнопки /start (главное меню модуля)\n  4. handle_command — обработчик команд\n  5. handle_callback — обработчик кнопок\n\nКонтракт:\n  TG_COMMANDS = {\n      \"command\": \"описание\"           # короткая форма = публичная, в меню\n      \"command2\": {\"desc\": \"...\", \"right\": \"some_right\", \"menu\": True/False},\n  }\n  TG_CALLBACKS = {\n      \"action\": \"описание\"\n  }\n  TG_MENU = [\n      {\"id\": \"btn_id\", \"title\": \"Кнопка\", \"callback\": \"action\", \"right\": \"...\"},\n  ]\n\n  def handle_command(ctx, config, command, text, inv) -> str | None:\n      # ctx — CoreContext, config — ModuleConfig\n      # command — строка команды, text — аргументы после команды\n      # inv = {\"chat_id\", \"user_qid\"}\n      # Возврат: строка (ответ) или None (без ответа)\n      ...\n\n  def handle_callback(ctx, config, action, args) -> str | tuple | None:\n      # action — строка из TG_CALLBACKS (без префикса модуля)\n      # args = {\"chat_id\", \"message_id\", \"message_text\", \"user_qid\"}\n      # Возврат:\n      #   str         — отредактировать сообщение с ответом\n      #   (\"edit\", t) — то же самое (явный формат)\n      #   (\"send\", t) — новое сообщение в чат\n      #   (\"toast\", t) — всплывашка (answer) без правки сообщения\n      #   None        — ничего не делать\n      ...\n\"\"\"\n\n# --- Команды бота ---\n# Простая строка = {\"desc\": строка, \"right\": None, \"menu\": True}\n# Словарь = полный контроль над поведением.\nTG_COMMANDS = {\n    \"ping\": \"проверить живость модуля\",\n    \"counter\": {\n        \"desc\": \"текущий счётчик + кнопки\",\n        \"right\": None,          # None = доступна всем\n        \"menu\": True,           # True = показать в /start\n    },\n    \"reset\": \"сбросить счётчик\",\n    \"fact\": \"получить случайный факт о bot_modular\",\n}\n\n# --- Кнопки модуля (колбэки) ---\n# Паттерн колбэка: <модуль>:<action> (макс. 64 байта)\nTG_CALLBACKS = {\n    \"increment\": {\"desc\": \"+1 к счётчику\", \"right\": None},\n    \"decrement\": {\"desc\": \"-1 к счётчику\", \"right\": None},\n    \"settings\": {\"desc\": \"настройки модуля\", \"right\": \"cookbook_manage\"},\n}\n\n# --- Кнопки /start (главное меню модуля) ---\n# Каждая кнопка: id, title, callback, право (опционально).\n# Транспорт собирает кнопки всех модулей в одно меню.\n# wide=True = кнопка на всю ширину. hidden=True = скрыта без права.\nTG_MENU = [\n    {\n        \"id\": \"cookbook_main\",\n        \"title\": \"📖 Cookbook\",\n        \"callback\": \"counter\",\n        \"right\": None,\n    },\n    {\n        \"id\": \"cookbook_settings\",\n        \"title\": \"⚙️ Настройки\",\n        \"callback\": \"settings\",\n        \"right\": \"cookbook_manage\",\n    },\n]\n\n\ndef handle_command(ctx, config, command: str, text: str, inv=None):\n    \"\"\"Обработчик Telegram-команд.\n\n    Args:\n        ctx: CoreContext — контекст ядра (api, events, rights, cache)\n        config: ModuleConfig — конфигурация модуля\n        command: str — имя команды (без /)\n        text: str — аргументы после команды\n        inv: dict | None — {\"chat_id\": int, \"user_qid\": str}\n\n    Returns:\n        str — текст ответа (разбивается на чанки по 4096 символов)\n        None — без ответа\n    \"\"\"\n    if command == \"ping\":\n        # --- Вызов API-метода другого модуля ---\n        # ctx.api.call(\"модуль.метод\", *args) — синхронный вызов.\n        # Неизвестный метод → UserError (400), а не крах.\n        try:\n            pong = ctx.api.call(\"cookbook.ping\")\n            return \"🟢 %s\" % pong\n        except Exception as e:\n            return \"❌ cookbook.ping failed: %s\" % e\n\n    if command == \"counter\":\n        # --- Счётчик с кнопками ---\n        # Callback-кнопки создаются через InlineKeyboardButton.\n        # Паттерн: \"<модуль>:<action>\" — транспорт роутит в handle_callback.\n        try:\n            data = ctx.api.call(\"cookbook.counter\")\n            count = data.get(\"count\", \"?\")\n            lines = [\n                \"📊 Счётчик cookbook\",\n                \"  count: %d\" % count,\n                \"\",\n                \"Кнопки: [ +1 ] [ -1 ]\",\n            ]\n            return \"\\n\".join(lines)\n        except Exception as e:\n            return \"❌ %s\" % e\n\n    if command == \"reset\":\n        # --- Сброс счётчика ---\n        try:\n            ctx.api.call(\"cookbook.reset\")\n            return \"🔄 Счётчик сброшен\"\n        except Exception as e:\n            return \"❌ %s\" % e\n\n    if command == \"fact\":\n        # --- Случайный факт ---\n        try:\n            fact = ctx.api.call(\"cookbook.fact\")\n            return \"📚 %s\" % fact\n        except Exception as e:\n            return \"❌ %s\" % e\n\n    # Неизвестная команда — None = без ответа (транспорт уже показал 400).\n    return None\n\n\ndef handle_callback(ctx, config, action: str, args: dict):\n    \"\"\"Обработчик callback-кнопок модуля.\n\n    Args:\n        ctx: CoreContext\n        config: ModuleConfig\n        action: str — имя действия (из TG_CALLBACKS, без префикса модуля)\n        args: dict — {\"chat_id\", \"message_id\", \"message_text\", \"user_qid\"}\n\n    Returns:\n        str | tuple | None:\n          str         — отредактировать сообщение\n          (\"edit\", t) — то же самое\n          (\"send\", t) — новое сообщение\n          (\"toast\", t) — всплывашка (answer)\n          None        — ничего\n    \"\"\"\n    if action == \"increment\":\n        # --- Увеличить счётчик ---\n        try:\n            data = ctx.api.call(\"cookbook.counter\")\n            new_count = data[\"count\"] + 1\n            return (\"toast\", \"+1 → %d\" % new_count)\n        except Exception as e:\n            return (\"toast\", \"Ошибка: %s\" % e)\n\n    if action == \"decrement\":\n        # --- Уменьшить счётчик ---\n        try:\n            data = ctx.api.call(\"cookbook.counter\")\n            new_count = max(0, data[\"count\"] - 1)\n            return (\"toast\", \"-1 → %d\" % new_count)\n        except Exception as e:\n            return (\"toast\", \"Ошибка: %s\" % e)\n\n    if action == \"settings\":\n        # --- Настройки (кнопка ⚙️ из TG_MENU) ---\n        try:\n            data = ctx.api.call(\"cookbook.settings_get\")\n            lines = [\"⚙️ Настройки cookbook:\"]\n            for k in sorted(set(list(data.get(\"defaults\") or {})\n                                     + list(data.get(\"overrides\") or []))):\n                lines.append(\"  %s = %s\" % (k, (data.get(\"overrides\") or {}).get(k, \"(default)\")))\n            return \"\\n\".join(lines)\n        except Exception as e:\n            return (\"toast\", \"Ошибка: %s\" % e)\n\n    return None\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"cookbook.ui_tg — Telegram-интерфейс модуля.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Демонстрирует все паттерны TG-интерфейса:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  1. TG_COMMANDS — команды бота (простая строка или словарь с meta)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  2. TG_CALLBACKS — кнопки модуля (колбэки с паттерном &#x3C;модуль>:&#x3C;action>)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  3. TG_MENU — кнопки /start (главное меню модуля)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  4. handle_command — обработчик команд</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  5. handle_callback — обработчик кнопок</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Контракт:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  TG_COMMANDS = {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      \"command\": \"описание\"           # короткая форма = публичная, в меню</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      \"command2\": {\"desc\": \"...\", \"right\": \"some_right\", \"menu\": True/False},</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  }</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  TG_CALLBACKS = {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      \"action\": \"описание\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  }</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  TG_MENU = [</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      {\"id\": \"btn_id\", \"title\": \"Кнопка\", \"callback\": \"action\", \"right\": \"...\"},</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  ]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  def handle_command(ctx, config, command, text, inv) -> str | None:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      # ctx — CoreContext, config — ModuleConfig</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      # command — строка команды, text — аргументы после команды</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      # inv = {\"chat_id\", \"user_qid\"}</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      # Возврат: строка (ответ) или None (без ответа)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      ...</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  def handle_callback(ctx, config, action, args) -> str | tuple | None:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      # action — строка из TG_CALLBACKS (без префикса модуля)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      # args = {\"chat_id\", \"message_id\", \"message_text\", \"user_qid\"}</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      # Возврат:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      #   str         — отредактировать сообщение с ответом</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      #   (\"edit\", t) — то же самое (явный формат)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      #   (\"send\", t) — новое сообщение в чат</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      #   (\"toast\", t) — всплывашка (answer) без правки сообщения</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      #   None        — ничего не делать</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      ...</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># --- Команды бота ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Простая строка = {\"desc\": строка, \"right\": None, \"menu\": True}</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Словарь = полный контроль над поведением.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TG_COMMANDS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"проверить живость модуля\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"текущий счётчик + кнопки\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,          </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># None = доступна всем</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,           </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># True = показать в /start</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    },</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"reset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"сбросить счётчик\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"fact\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"получить случайный факт о bot_modular\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># --- Кнопки модуля (колбэки) ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Паттерн колбэка: &#x3C;модуль>:&#x3C;action> (макс. 64 байта)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TG_CALLBACKS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"increment\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"+1 к счётчику\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"decrement\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-1 к счётчику\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"settings\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"настройки модуля\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># --- Кнопки /start (главное меню модуля) ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Каждая кнопка: id, title, callback, право (опционально).</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Транспорт собирает кнопки всех модулей в одно меню.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># wide=True = кнопка на всю ширину. hidden=True = скрыта без права.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TG_MENU</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_main\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📖 Cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"callback\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    },</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_settings\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"⚙️ Настройки\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"callback\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"settings\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    },</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> handle_command</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx, config, command: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, text: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, inv</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Обработчик Telegram-команд.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Args:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        ctx: CoreContext — контекст ядра (api, events, rights, cache)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        config: ModuleConfig — конфигурация модуля</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        command: str — имя команды (без /)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        text: str — аргументы после команды</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        inv: dict | None — {\"chat_id\": int, \"user_qid\": str}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Returns:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        str — текст ответа (разбивается на чанки по 4096 символов)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        None — без ответа</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> command </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # --- Вызов API-метода другого модуля ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # ctx.api.call(\"модуль.метод\", *args) — синхронный вызов.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Неизвестный метод → UserError (400), а не крах.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            pong </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"🟢 </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pong</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"❌ cookbook.ping failed: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> command </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # --- Счётчик с кнопками ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Callback-кнопки создаются через InlineKeyboardButton.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Паттерн: \"&#x3C;модуль>:&#x3C;action>\" — транспорт роутит в handle_callback.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            count </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"?\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            lines </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"📊 Счётчик cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"  count: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> count,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"Кнопки: [ +1 ] [ -1 ]\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(lines)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"❌ </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> command </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"reset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # --- Сброс счётчика ---</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.reset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"🔄 Счётчик сброшен\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"❌ </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> command </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"fact\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # --- Случайный факт ---</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            fact </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.fact\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"📚 </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fact</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"❌ </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # Неизвестная команда — None = без ответа (транспорт уже показал 400).</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> handle_callback</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx, config, action: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, args: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Обработчик callback-кнопок модуля.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Args:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        ctx: CoreContext</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        config: ModuleConfig</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        action: str — имя действия (из TG_CALLBACKS, без префикса модуля)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        args: dict — {\"chat_id\", \"message_id\", \"message_text\", \"user_qid\"}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Returns:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        str | tuple | None:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          str         — отредактировать сообщение</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          (\"edit\", t) — то же самое</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          (\"send\", t) — новое сообщение</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          (\"toast\", t) — всплывашка (answer)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          None        — ничего</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> action </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"increment\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # --- Увеличить счётчик ---</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            new_count </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toast\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"+1 → </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_count)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toast\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> action </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"decrement\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # --- Уменьшить счётчик ---</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            new_count </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> max</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, data[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toast\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-1 → </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_count)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toast\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> action </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"settings\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # --- Настройки (кнопка ⚙️ из TG_MENU) ---</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.settings_get\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            lines </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"⚙️ Настройки cookbook:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">list</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"defaults\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {})</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                                     +</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> list</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"overrides\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []))):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                lines.append(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> = </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (k, (data.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"overrides\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}).get(k, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"(default)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(lines)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toast\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "ui_web.py",
          "size": 10852,
          "content": "\"\"\"cookbook.ui_web — Web-интерфейс модуля.\n\nДемонстрирует все паттерны Web-интерфейса:\n  1. ROUTES — REST API модуля\n  2. SERVICE — карточка в кабинете (каталог сервисов /api/services)\n  3. PANELS — витрина для SPA (этап WEB-3)\n  4. MONITOR_PANELS — карточка в панели мониторинга\n  5. NAV — пункт навигации шапки SPA\n  6. handle_api — обработчик API-запросов\n\nКонтракт ROUTES:\n  ROUTES = [\n      (\n          \"GET\",                              # HTTP-метод\n          \"/api/cookbook/ping\",               # путь (обязан начинаться с /api/<mod>/)\n          \"ping\",                             # имя метода в handle_api\n          {                                   # метаданные (опционально)\n              \"right\": None,                  # None = публичный, \"cookbook_private\" = gated\n              \"desc\": \"Проверка живости\",     # описание для /api/docs\n          },\n      ),\n  ]\n\n  handle_api(ctx, config, method, req) -> dict | (status, obj) | (status, obj, extra)\n    ctx: CoreContext\n    config: ModuleConfig | None\n    method: str — имя метода (третий элемент ROUTES)\n    req: dict — {\n        \"http_method\": \"GET\"/\"POST\"/...\n        \"path\": \"/api/cookbook/ping\"\n        \"query\": {\"k\": \"v\"}  # из ?k=v\n        \"body\": {...}        # JSON из тела (POST/PUT)\n        \"files\": {...}       # multipart/form-data\n        \"user_qid\": \"web:123\" | None\n        \"session\": {...}     # данные сессии (токен/кука)\n        \"remote_ip\": \"...\"\n    }\n\n    Возврат:\n      dict/list/str           → 200 OK, JSON\n      (status, obj)           → свой HTTP-статус\n      (status, obj, extra)    → extra = {\"set_cookies\": [...], \"clear_cookies\": [...]}\n      UserError               → 400 {\"ok\": false, \"error\": \"...\"}\n      BotError                → 500 {\"ok\": false, \"error\": \"internal\"}\n\nПравила изоляции:\n  - Путь обязан начинаться с /api/cookbook/ (иначе роут отклоняется)\n  - Право обязано быть декларировано в manifest.yaml (иначе warning)\n\"\"\"\n\n# --- REST API модуля ---\nROUTES = [\n    # Публичные роуты (right=None)\n    (\"GET\", \"/api/cookbook/ping\", \"ping\", {\"right\": None, \"desc\": \"Проверка живости модуля\"}),\n    (\"GET\", \"/api/cookbook/counter\", \"counter\", {\"right\": None, \"desc\": \"Текущий счётчик\"}),\n    (\"GET\", \"/api/cookbook/fact\", \"fact\", {\"right\": None, \"desc\": \"Случайный факт\"}),\n    (\"GET\", \"/api/cookbook/health\", \"health\", {\"right\": None, \"desc\": \"Здоровье для панели мониторинга\"}),\n\n    # Gated роуты (требуют право)\n    (\"POST\", \"/api/cookbook/reset\", \"reset\", {\"right\": None, \"desc\": \"Сбросить счётчик\"}),\n    (\"GET\", \"/api/cookbook/settings\", \"settings_get\", {\"right\": \"cookbook_manage\", \"desc\": \"Настройки модуля\"}),\n    (\"POST\", \"/api/cookbook/settings\", \"settings_set\", {\"right\": \"cookbook_manage\", \"desc\": \"Записать настройку {key, value}\"}),\n\n    # Приватный роут (требует cookbook_private)\n    (\"GET\", \"/api/cookbook/private\", \"private\", {\"right\": \"cookbook_private\", \"desc\": \"Приватный endpoint (cookbook_private)\"}),\n]\n\n# --- Карточки кабинета (каталог /api/services) ---\n# SERVICE может быть dict или list dict.\n# key — уникальный ключ, icon — эмодзи, phase — этап готовности (1 = готов).\nSERVICE = [\n    {\n        \"key\": \"cookbook\",\n        \"icon\": \"📖\",\n        \"title\": \"Cookbook — эталон модуля\",\n        \"right\": None,\n        \"phase\": 1,\n        \"desc\": \"Эталонный модуль-справочник: все паттерны разработки\",\n    },\n]\n\n# --- Витрина для SPA (устарело: панели мониторинга — через MONITOR_PANELS) ---\nPANELS = []\n\n# --- Панели мониторинга ---\n# Монтируются в общую сетку мониторинга (GET /api/monitoring/panels).\n# visibility: \"public\" (все) | \"auth\" (вошедшие) | \"right:<право>\".\n# refresh_s — интервал обновления в секундах.\nMONITOR_PANELS = [\n    {\n        \"id\": \"cookbook_counter\",\n        \"title\": \"Счётчик cookbook\",\n        \"api\": \"/api/cookbook/counter\",\n        \"visibility\": \"public\",\n        \"refresh_s\": 5,\n    },\n    {\n        \"id\": \"cookbook_health\",\n        \"title\": \"Здоровье cookbook\",\n        \"api\": \"/api/cookbook/health\",\n        \"visibility\": \"auth\",\n        \"refresh_s\": 15,\n    },\n]\n\n# --- Навигация шапки SPA ---\n# Пункты меню в верхней панели (GET /api/nav).\n# view — имя вьюхи SPA (showView), href — внешняя ссылка.\n# Одно из view/href обязательно. right — gated-навигация.\nNAV = [\n    {\n        \"id\": \"cookbook\",\n        \"title\": \"📖 Cookbook\",\n        \"view\": \"cookbook\",\n        \"icon\": \"📖\",\n        \"right\": None,  # None = всем, \"cookbook_manage\" = только с правом\n    },\n]\n\n\ndef handle_api(ctx, config, method: str, req: dict):\n    \"\"\"Обработчик API-запросов модуля.\n\n    Единая точка для всех ROUTES. Диспетчеризация по имени метода.\n\n    Args:\n        ctx: CoreContext — контекст ядра\n        config: ModuleConfig | None — конфигурация модуля\n        method: str — имя метода (из ROUTES)\n        req: dict — данные запроса\n\n    Returns:\n        dict | (status, obj) | (status, obj, extra)\n    \"\"\"\n    # --- Импорт внутри функции против циклических импортов ---\n    from core.errors import UserError\n\n    # --- Аутентификация и права ---\n    # req[\"session\"] — данные сессии (из Bearer/cookie)\n    # req[\"user_qid\"] — qualified_id (web:123 или None)\n    sess = req.get(\"session\") or {}\n    user_qid = req.get(\"user_qid\")\n\n    # --- Проверка права через API транспорта ---\n    # ctx.api.call(\"transport_web.check\", sess, right) — стандартный паттерн.\n    # Можно также: ctx.rights.can(user_qid, right)\n    def _has_right(right_key: str) -> bool:\n        \"\"\"Проверить право пользователя.\"\"\"\n        if not sess:\n            return False\n        try:\n            return bool(ctx.api.call(\"transport_web.check\", sess, right_key))\n        except Exception:\n            return False\n\n    # --- Методы ---\n\n    if method == \"ping\":\n        # Простейший ответ — dict → 200 OK\n        return {\"ok\": True, \"module\": \"cookbook\", \"pong\": \"pong\"}\n\n    if method == \"counter\":\n        # Вызов API-метода другого модуля\n        try:\n            data = ctx.api.call(\"cookbook.counter\")\n            return {\"ok\": True, \"counter\": data}\n        except Exception as e:\n            return 500, {\"ok\": False, \"error\": str(e)[:400]}\n\n    if method == \"reset\":\n        # POST — читаем body\n        body = req.get(\"body\") or {}\n        # Можно валидировать входные данные\n        force = body.get(\"force\", False)\n        if not isinstance(force, bool):\n            return 400, {\"ok\": False, \"error\": \"force должен быть boolean\"}\n        try:\n            ctx.api.call(\"cookbook.reset\")\n            return {\"ok\": True, \"message\": \"Счётчик сброшен\"}\n        except Exception as e:\n            return 500, {\"ok\": False, \"error\": str(e)[:400]}\n\n    if method == \"fact\":\n        # Простой API\n        try:\n            fact = ctx.api.call(\"cookbook.fact\")\n            return {\"ok\": True, \"fact\": fact}\n        except Exception as e:\n            return 500, {\"ok\": False, \"error\": str(e)[:400]}\n\n    if method == \"settings_get\":\n        # Gated — проверяем право\n        if not _has_right(\"cookbook_manage\"):\n            return 403, {\"ok\": False, \"error\": \"forbidden\", \"right\": \"cookbook_manage\"}\n        try:\n            settings = ctx.api.call(\"cookbook.settings_get\")\n            return {\"ok\": True, \"settings\": settings}\n        except Exception as e:\n            return 500, {\"ok\": False, \"error\": str(e)[:400]}\n\n    if method == \"settings_set\":\n        # Gated + валидация body\n        if not _has_right(\"cookbook_manage\"):\n            return 403, {\"ok\": False, \"error\": \"forbidden\", \"right\": \"cookbook_manage\"}\n        body = req.get(\"body\") or {}\n        key = (body.get(\"key\") or \"\").strip()\n        value = (body.get(\"value\") or \"\").strip()\n        if not key or not value:\n            return 400, {\"ok\": False, \"error\": \"нужны key и value\"}\n        try:\n            ctx.api.call(\"cookbook.settings_set\", key, value)\n            return {\"ok\": True, \"key\": key, \"message\": \"Настройка обновлена\"}\n        except UserError as e:\n            # UserError → 400 (автоматически, но здесь ловим явно)\n            return 400, {\"ok\": False, \"error\": str(e)}\n        except Exception as e:\n            return 500, {\"ok\": False, \"error\": str(e)[:400]}\n\n    if method == \"private\":\n        # Приватный роут — только для пользователей с cookbook_private\n        # Право проверяется транспортом ДО вызова handle_api (см. collect.py).\n        # Здесь — дополнительная проверка для примера.\n        if not _has_right(\"cookbook_private\"):\n            return 403, {\"ok\": False, \"error\": \"forbidden\", \"right\": \"cookbook_private\"}\n        return {\n            \"ok\": True,\n            \"message\": \"Вы добрались до приватного endpoint!\",\n            \"user\": user_qid,\n        }\n\n    if method == \"health\":\n        # Для панели мониторинга (MONITOR_PANELS ниже ссылается сюда).\n        # Право — по панели (visibility), сам роут публичный.\n        try:\n            data = ctx.api.call(\"cookbook.counter\")\n            return {\"ok\": True, \"count\": data.get(\"count\", 0)}\n        except Exception as e:\n            return 500, {\"ok\": False, \"error\": str(e)[:400]}\n\n    # Неизвестный метод — UserError → 400\n    raise UserError(\"неизвестный метод: %s\" % method)\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"cookbook.ui_web — Web-интерфейс модуля.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Демонстрирует все паттерны Web-интерфейса:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  1. ROUTES — REST API модуля</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  2. SERVICE — карточка в кабинете (каталог сервисов /api/services)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  3. PANELS — витрина для SPA (этап WEB-3)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  4. MONITOR_PANELS — карточка в панели мониторинга</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  5. NAV — пункт навигации шапки SPA</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  6. handle_api — обработчик API-запросов</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Контракт ROUTES:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  ROUTES = [</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      (</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          \"GET\",                              # HTTP-метод</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          \"/api/cookbook/ping\",               # путь (обязан начинаться с /api/&#x3C;mod>/)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          \"ping\",                             # имя метода в handle_api</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          {                                   # метаданные (опционально)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">              \"right\": None,                  # None = публичный, \"cookbook_private\" = gated</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">              \"desc\": \"Проверка живости\",     # описание для /api/docs</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          },</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      ),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  ]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  handle_api(ctx, config, method, req) -> dict | (status, obj) | (status, obj, extra)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    ctx: CoreContext</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    config: ModuleConfig | None</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    method: str — имя метода (третий элемент ROUTES)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    req: dict — {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"http_method\": \"GET\"/\"POST\"/...</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"path\": \"/api/cookbook/ping\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"query\": {\"k\": \"v\"}  # из ?k=v</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"body\": {...}        # JSON из тела (POST/PUT)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"files\": {...}       # multipart/form-data</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"user_qid\": \"web:123\" | None</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"session\": {...}     # данные сессии (токен/кука)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"remote_ip\": \"...\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Возврат:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      dict/list/str           → 200 OK, JSON</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      (status, obj)           → свой HTTP-статус</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      (status, obj, extra)    → extra = {\"set_cookies\": [...], \"clear_cookies\": [...]}</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      UserError               → 400 {\"ok\": false, \"error\": \"...\"}</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      BotError                → 500 {\"ok\": false, \"error\": \"internal\"}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Правила изоляции:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  - Путь обязан начинаться с /api/cookbook/ (иначе роут отклоняется)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  - Право обязано быть декларировано в manifest.yaml (иначе warning)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># --- REST API модуля ---</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ROUTES</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # Публичные роуты (right=None)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Проверка живости модуля\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Текущий счётчик\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/fact\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"fact\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Случайный факт\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/health\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"health\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Здоровье для панели мониторинга\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # Gated роуты (требуют право)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/reset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"reset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Сбросить счётчик\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/settings\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"settings_get\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Настройки модуля\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/settings\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"settings_set\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Записать настройку {key, value}\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # Приватный роут (требует cookbook_private)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/private\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"private\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_private\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Приватный endpoint (cookbook_private)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># --- Карточки кабинета (каталог /api/services) ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># SERVICE может быть dict или list dict.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># key — уникальный ключ, icon — эмодзи, phase — этап готовности (1 = готов).</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">SERVICE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"icon\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📖\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Cookbook — эталон модуля\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"phase\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Эталонный модуль-справочник: все паттерны разработки\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    },</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># --- Витрина для SPA (устарело: панели мониторинга — через MONITOR_PANELS) ---</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">PANELS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># --- Панели мониторинга ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Монтируются в общую сетку мониторинга (GET /api/monitoring/panels).</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># visibility: \"public\" (все) | \"auth\" (вошедшие) | \"right:&#x3C;право>\".</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># refresh_s — интервал обновления в секундах.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">MONITOR_PANELS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Счётчик cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"api\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"visibility\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"public\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"refresh_s\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    },</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_health\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Здоровье cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"api\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/health\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"visibility\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"auth\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"refresh_s\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">15</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    },</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># --- Навигация шапки SPA ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Пункты меню в верхней панели (GET /api/nav).</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># view — имя вьюхи SPA (showView), href — внешняя ссылка.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Одно из view/href обязательно. right — gated-навигация.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">NAV</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📖 Cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"view\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"icon\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📖\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># None = всем, \"cookbook_manage\" = только с правом</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    },</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> handle_api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx, config, method: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, req: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Обработчик API-запросов модуля.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Единая точка для всех ROUTES. Диспетчеризация по имени метода.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Args:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        ctx: CoreContext — контекст ядра</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        config: ModuleConfig | None — конфигурация модуля</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        method: str — имя метода (из ROUTES)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        req: dict — данные запроса</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Returns:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        dict | (status, obj) | (status, obj, extra)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # --- Импорт внутри функции против циклических импортов ---</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.errors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # --- Аутентификация и права ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # req[\"session\"] — данные сессии (из Bearer/cookie)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # req[\"user_qid\"] — qualified_id (web:123 или None)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    sess </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"session\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    user_qid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"user_qid\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # --- Проверка права через API транспорта ---</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # ctx.api.call(\"transport_web.check\", sess, right) — стандартный паттерн.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # Можно также: ctx.rights.can(user_qid, right)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _has_right</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(right_key: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Проверить право пользователя.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sess:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"transport_web.check\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, sess, right_key))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # --- Методы ---</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Простейший ответ — dict → 200 OK</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"module\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"pong\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"pong\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Вызов API-метода другого модуля</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: data}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"reset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # POST — читаем body</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Можно валидировать входные данные</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        force </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"force\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(force, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"force должен быть boolean\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.reset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"message\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Счётчик сброшен\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"fact\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Простой API</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            fact </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.fact\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"fact\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: fact}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"settings_get\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Gated — проверяем право</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _has_right(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 403</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"forbidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            settings </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.settings_get\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"settings\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: settings}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"settings_set\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Gated + валидация body</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _has_right(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 403</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"forbidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"value\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> value:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"нужны key и value\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.settings_set\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, key, value)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: key, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"message\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Настройка обновлена\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # UserError → 400 (автоматически, но здесь ловим явно)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"private\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Приватный роут — только для пользователей с cookbook_private</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Право проверяется транспортом ДО вызова handle_api (см. collect.py).</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Здесь — дополнительная проверка для примера.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _has_right(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_private\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 403</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"forbidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook_private\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"message\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Вы добрались до приватного endpoint!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"user\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: user_qid,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"health\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Для панели мониторинга (MONITOR_PANELS ниже ссылается сюда).</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Право — по панели (visibility), сам роут публичный.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: data.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # Неизвестный метод — UserError → 400</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"неизвестный метод: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method)</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "ui_ws.py",
          "size": 7343,
          "content": "'''cookbook.ui_ws — WebSocket-интерфейс модуля.\n\nДемонстрирует паттерн WebSocket для модуля:\n  WS_PATH = \"/ws/cookbook/ticker\"  # путь (обязан начинаться с /ws/)\n  class WSSocket(BaseSocket)       # наследник базы транспорта\n\nКонтракт:\n  WS_PATH = \"/ws/<namespace>/<name>\"\n    # Путь обязан начинаться с /ws/ и быть задекларирован в manifest.yaml\n    # ws_routes: [\"/ws/cookbook/ticker\"]\n\n  class WSSocket(BaseSocket):\n      RIGHT = \"cookbook_manage\"    # право для подключения (None = всем)\n\n      def on_authed(self, sess):\n          # Точка входа после успешной аутентификации\n          # self.mod_ctx — CoreContext\n          # self.ws_sess — данные сессии (из ticket/Bearer/cookie)\n          # self.write_message() — отправить сообщение\n          # self.close(code, reason) — закрыть соединение\n          ...\n\n      def on_message(self, raw: str):\n          # Обработка входящего сообщения (JSON или текст)\n          # raw — строка от клиента\n          ...\n\n      def on_close(self):\n          # Очистка при закрытии\n          ...\n\nBaseSocket (из transport_web.ws):\n  - check_origin() — проверка origin (same-host по умолчанию)\n  - ws_session() — ticket -> Bearer -> cookie\n  - ws_has_right(key) — проверка права\n  - write_message() — отправить текст/JSON\n  - close(code, reason) — закрыть соединение\n'''\n\n# --- Путь WebSocket (декларируется в manifest.yaml: ws_routes) ---\nWS_PATH = \"/ws/cookbook/ticker\"\n\n# --- Импортируем базовый класс из транспорта ---\nfrom botmod_transport_web.ws import make_base as _make_base\n\nBaseSocket = _make_base()\n\n# --- Состояние сессий (ключ: \"web:<uid>\") ---\n_sessions = {}\n\n\nclass WSSocket(BaseSocket):\n    \"\"\"WebSocket-сокет для стриминга тиков счётчика.\"\"\"\n\n    # Право, проверяемое при open и перед каждым запросом.\n    # None = всем, \"cookbook_manage\" = только с правом.\n    RIGHT = \"cookbook_manage\"\n\n    def on_authed(self, sess):\n        \"\"\"Подключение установлено, пользователь аутентифицирован.\n\n        Точка входа для инициализации сессии:\n          - self.mod_ctx — CoreContext\n          - self.ws_sess — данные сессии (из ticket/Bearer/cookie)\n          - self.write_message() — отправить сообщение клиенту\n\n        Открываем поток-читатель для стриминга тиков.\n        \"\"\"\n        import json\n        import threading\n\n        self.ws_uid = sess[\"uid\"]\n        self._stop = False\n\n        # Регистрируем сессию\n        key = \"web:%s\" % self.ws_uid\n        _sessions[key] = self\n\n        # Отправляем приветствие\n        self.write_message(json.dumps({\n            \"type\": \"connected\",\n            \"uid\": self.ws_uid,\n            \"message\": \"📖 cookbook ticker подключён\",\n        }, ensure_ascii=False))\n\n        # Запускаем поток для стриминга тиков\n        self._stream_thread = threading.Thread(\n            target=self._ticker_stream,\n            daemon=True,\n            name=\"cookbook:ws_stream\",\n        )\n        self._stream_thread.start()\n\n    def _ticker_stream(self):\n        \"\"\"Фоновый поток: стриминг тиков счётчика.\"\"\"\n        import json\n        import time\n        import logging\n\n        log = logging.getLogger(\"botmod.cookbook.ws\")\n        while not self._stop:\n            try:\n                # Читаем текущий счётчик через API\n                data = self.mod_ctx.api.call(\"cookbook.counter\")\n                count = data.get(\"count\", 0)\n                self.write_message(json.dumps({\n                    \"type\": \"tick\",\n                    \"count\": count,\n                }, ensure_ascii=False))\n            except Exception as e:\n                log.error(\"ticker stream error: %s\", e)\n                break\n            # Стримим каждую секунду\n            time.sleep(1)\n\n    def on_message(self, raw: str):\n        \"\"\"Обработка входящих сообщений от клиента.\n\n        Клиент может отправить:\n          {\"type\": \"ping\"} → ответим {\"type\": \"pong\"}\n          {\"type\": \"subscribe\"} → начнём стриминг (если не запущен)\n          {\"type\": \"unsubscribe\"} → остановим стриминг\n          любой текст → эхо\n\n        raw — строка от клиента (JSON или текст).\n        \"\"\"\n        import json\n\n        try:\n            msg = json.loads(raw)\n        except (json.JSONDecodeError, ValueError):\n            # Не JSON — отправляем как есть (эхо)\n            self.write_message(raw)\n            return\n\n        msg_type = msg.get(\"type\", \"\")\n\n        if msg_type == \"ping\":\n            # Pong - проверка связи\n            import time as _time\n            self.write_message(json.dumps({\n                \"type\": \"pong\",\n                \"timestamp\": _time.time(),\n            }, ensure_ascii=False))\n\n        elif msg_type == \"subscribe\":\n            # Стриминг уже запущен в on_authed — подтверждаем\n            self.write_message(json.dumps({\n                \"type\": \"subscribed\",\n                \"message\": \"Тик-стриминг активен\",\n            }, ensure_ascii=False))\n\n        elif msg_type == \"unsubscribe\":\n            # Останавливаем стриминг (в on_close всё равно остановится)\n            self.write_message(json.dumps({\n                \"type\": \"unsubscribed\",\n            }, ensure_ascii=False))\n\n        elif msg_type == \"reset\":\n            # Сброс счётчика через API\n            try:\n                self.mod_ctx.api.call(\"cookbook.reset\")\n                self.write_message(json.dumps({\n                    \"type\": \"reset_ok\",\n                }, ensure_ascii=False))\n            except Exception as e:\n                self.write_message(json.dumps({\n                    \"type\": \"error\",\n                    \"message\": str(e)[:200],\n                }, ensure_ascii=False))\n\n        else:\n            # Неизвестный тип — ошибка\n            self.write_message(json.dumps({\n                \"type\": \"error\",\n                \"message\": \"неизвестный тип сообщения: %s\" % msg_type,\n            }, ensure_ascii=False))\n\n    def on_close(self):\n        \"\"\"Очистка при закрытии WebSocket.\"\"\"\n        import logging\n\n        key = \"web:%s\" % getattr(self, \"ws_uid\", \"?\")\n        _sessions.pop(key, None)\n        self._stop = True\n        logging.getLogger(\"botmod.cookbook.ws\").info(\n            \"WS ticker закрыт: %s\", key\n        )\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'''cookbook.ui_ws — WebSocket-интерфейс модуля.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Демонстрирует паттерн WebSocket для модуля:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  WS_PATH = \"/ws/cookbook/ticker\"  # путь (обязан начинаться с /ws/)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  class WSSocket(BaseSocket)       # наследник базы транспорта</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Контракт:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  WS_PATH = \"/ws/&#x3C;namespace>/&#x3C;name>\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    # Путь обязан начинаться с /ws/ и быть задекларирован в manifest.yaml</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    # ws_routes: [\"/ws/cookbook/ticker\"]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  class WSSocket(BaseSocket):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      RIGHT = \"cookbook_manage\"    # право для подключения (None = всем)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      def on_authed(self, sess):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          # Точка входа после успешной аутентификации</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          # self.mod_ctx — CoreContext</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          # self.ws_sess — данные сессии (из ticket/Bearer/cookie)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          # self.write_message() — отправить сообщение</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          # self.close(code, reason) — закрыть соединение</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          ...</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      def on_message(self, raw: str):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          # Обработка входящего сообщения (JSON или текст)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          # raw — строка от клиента</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          ...</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      def on_close(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          # Очистка при закрытии</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          ...</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">BaseSocket (из transport_web.ws):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  - check_origin() — проверка origin (same-host по умолчанию)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  - ws_session() — ticket -> Bearer -> cookie</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  - ws_has_right(key) — проверка права</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  - write_message() — отправить текст/JSON</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  - close(code, reason) — закрыть соединение</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'''</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># --- Путь WebSocket (декларируется в manifest.yaml: ws_routes) ---</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">WS_PATH</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"/ws/cookbook/ticker\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># --- Импортируем базовый класс из транспорта ---</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> botmod_transport_web.ws </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> make_base </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _make_base</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">BaseSocket </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _make_base()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># --- Состояние сессий (ключ: \"web:&#x3C;uid>\") ---</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">_sessions </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">class</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> WSSocket</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">BaseSocket</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"WebSocket-сокет для стриминга тиков счётчика.\"\"\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # Право, проверяемое при open и перед каждым запросом.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # None = всем, \"cookbook_manage\" = только с правом.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    RIGHT</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"cookbook_manage\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> on_authed</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, sess):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Подключение установлено, пользователь аутентифицирован.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Точка входа для инициализации сессии:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          - self.mod_ctx — CoreContext</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          - self.ws_sess — данные сессии (из ticket/Bearer/cookie)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          - self.write_message() — отправить сообщение клиенту</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Открываем поток-читатель для стриминга тиков.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ws_uid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sess[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"uid\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._stop </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Регистрируем сессию</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"web:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ws_uid</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        _sessions[key] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Отправляем приветствие</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.write_message(json.dumps({</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"uid\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ws_uid,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"message\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📖 cookbook ticker подключён\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">ensure_ascii</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Запускаем поток для стриминга тиков</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._stream_thread </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading.Thread(</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            target</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._ticker_stream,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            daemon</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            name</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook:ws_stream\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        )</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._stream_thread.start()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _ticker_stream</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Фоновый поток: стриминг тиков счётчика.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> time</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        log </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging.getLogger(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"botmod.cookbook.ws\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        while</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._stop:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                # Читаем текущий счётчик через API</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.mod_ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                count </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.write_message(json.dumps({</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"tick\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: count,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                }, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">ensure_ascii</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                log.error(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ticker stream error: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, e)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                break</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Стримим каждую секунду</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            time.sleep(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> on_message</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, raw: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Обработка входящих сообщений от клиента.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Клиент может отправить:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          {\"type\": \"ping\"} → ответим {\"type\": \"pong\"}</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          {\"type\": \"subscribe\"} → начнём стриминг (если не запущен)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          {\"type\": \"unsubscribe\"} → остановим стриминг</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          любой текст → эхо</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        raw — строка от клиента (JSON или текст).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json.loads(raw)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (json.JSONDecodeError, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Не JSON — отправляем как есть (эхо)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.write_message(raw)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        msg_type </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg_type </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Pong - проверка связи</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> time </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _time</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.write_message(json.dumps({</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"pong\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"timestamp\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _time.time(),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            }, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">ensure_ascii</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg_type </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"subscribe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Стриминг уже запущен в on_authed — подтверждаем</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.write_message(json.dumps({</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"subscribed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"message\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Тик-стриминг активен\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            }, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">ensure_ascii</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg_type </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"unsubscribe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Останавливаем стриминг (в on_close всё равно остановится)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.write_message(json.dumps({</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"unsubscribed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            }, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">ensure_ascii</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg_type </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"reset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Сброс счётчика через API</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.mod_ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cookbook.reset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.write_message(json.dumps({</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"reset_ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                }, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">ensure_ascii</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.write_message(json.dumps({</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"message\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">200</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                }, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">ensure_ascii</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Неизвестный тип — ошибка</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.write_message(json.dumps({</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"message\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"неизвестный тип сообщения: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg_type,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            }, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">ensure_ascii</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> on_close</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Очистка при закрытии WebSocket.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"web:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ws_uid\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"?\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        _sessions.pop(key, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._stop </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        logging.getLogger(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"botmod.cookbook.ws\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).info(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"WS ticker закрыт: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, key</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        )</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "web/view.html",
          "size": 1567,
          "content": "<!-- Модульная вьюха cookbook: web/view.html + web/view.js.\n     Секция инжектится в <main>, скрипт грузится после app.js.\n     id секции: view-<view>, где <view> — второй аргумент registerService. -->\n<section id=\"view-cookbook\" class=\"hidden\">\n  <h2>📖 Cookbook</h2>\n  <p class=\"muted\">Эталонный модуль: счётчик, факт, настройки. Карточка в кабинете — через <span class=\"mono\">SERVICE</span> в <span class=\"mono\">ui_web.py</span>.</p>\n  <div class=\"card\">\n    <h3>Счётчик</h3>\n    <div class=\"mono\" id=\"cb-counter\">…</div>\n    <div class=\"toolbar\" style=\"margin-top:8px\">\n      <button class=\"btn-mini\" onclick=\"cbRefresh(this)\">Обновить</button>\n      <button class=\"btn-mini\" onclick=\"cbReset(this)\">Сбросить</button>\n      <button class=\"btn-mini\" onclick=\"cbFact(this)\">Случайный факт</button>\n    </div>\n    <div class=\"mono\" id=\"cb-out\" style=\"font-size:12px\"></div>\n  </div>\n  <div class=\"card\" style=\"margin-top:12px\">\n    <h3>Настройки <span class=\"muted\">(право cookbook_manage)</span></h3>\n    <div class=\"toolbar\">\n      <input id=\"cb-set-key\" placeholder=\"ключ\" style=\"width:200px\">\n      <input id=\"cb-set-val\" placeholder=\"значение\" style=\"flex:1;min-width:160px\">\n      <button class=\"btn-mini\" onclick=\"cbSetSave(this)\">Сохранить</button>\n    </div>\n    <div class=\"mono\" id=\"cb-set-out\" style=\"font-size:12px\"></div>\n  </div>\n</section>\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">&#x3C;!-- Модульная вьюха cookbook: web/view.html + web/view.js.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">     Секция инжектится в &#x3C;main>, скрипт грузится после app.js.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">     id секции: view-&#x3C;view>, где &#x3C;view> — второй аргумент registerService. --></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">section</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"view-cookbook\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>📖 Cookbook&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Эталонный модуль: счётчик, факт, настройки. Карточка в кабинете — через &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>SERVICE&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">> в &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>ui_web.py&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>.&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"card\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Счётчик&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cb-counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>…&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toolbar\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"margin-top:8px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> onclick</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">cbRefresh</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">this</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Обновить&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> onclick</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">cbReset</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">this</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Сбросить&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> onclick</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">cbFact</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">this</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Случайный факт&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cb-out\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"font-size:12px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"card\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"margin-top:12px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Настройки &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>(право cookbook_manage)&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toolbar\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">input</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cb-set-key\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> placeholder</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ключ\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"width:200px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">input</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cb-set-val\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> placeholder</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"значение\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"flex:1;min-width:160px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> onclick</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">cbSetSave</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">this</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Сохранить&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cb-set-out\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"font-size:12px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">section</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "web/view.js",
          "size": 2680,
          "content": "/* cookbook — веб-кабинет модуля (modules/cookbook/web/view.js).\n   Конвенция: load-функция тянет /api/<mod>/*, вьюха регистрируется сама —\n   в app.js ядра руками ничего не добавляется. esc()/escJs() — из app.js. */\n\nasync function loadCookbook() {\n  await cbRefresh();\n}\n\nasync function cbRefresh(btn) {\n  if (btn) btn.disabled = true;\n  try {\n    const r = await api(\"/api/cookbook/counter\");\n    if (!r.ok) { document.querySelector(\"#cb-counter\").textContent = \"Ошибка: \" + ((r.data && r.data.error) || \"\"); return; }\n    const c = r.data.counter || {};\n    document.querySelector(\"#cb-counter\").textContent = \"count = \" + (c.count ?? \"?\");\n  } finally {\n    if (btn) btn.disabled = false;\n  }\n}\n\nasync function cbReset(btn) {\n  if (btn) btn.disabled = true;\n  try {\n    const r = await api(\"/api/cookbook/reset\", { method: \"POST\", body: JSON.stringify({}) });\n    document.querySelector(\"#cb-out\").textContent = r.ok ? \"Сброшен\" : (\"Ошибка: \" + ((r.data && r.data.error) || \"\"));\n    await cbRefresh();\n  } finally {\n    if (btn) btn.disabled = false;\n  }\n}\n\nasync function cbFact(btn) {\n  if (btn) btn.disabled = true;\n  try {\n    const r = await api(\"/api/cookbook/fact\");\n    document.querySelector(\"#cb-out\").textContent = r.ok ? r.data.fact : (\"Ошибка: \" + ((r.data && r.data.error) || \"\"));\n  } finally {\n    if (btn) btn.disabled = false;\n  }\n}\n\nasync function cbSetSave(btn) {\n  const key = (document.querySelector(\"#cb-set-key\").value || \"\").trim();\n  const val = (document.querySelector(\"#cb-set-val\").value || \"\").trim();\n  if (!key || !val) { showToast(\"Нужны ключ и значение\"); return; }\n  if (btn) btn.disabled = true;\n  try {\n    const r = await api(\"/api/cookbook/settings\", { method: \"POST\", body: JSON.stringify({ key, value: val }) });\n    document.querySelector(\"#cb-set-out\").textContent = r.ok ? \"Сохранено: \" + key : (\"Ошибка: \" + ((r.data && r.data.error) || \"\"));\n  } finally {\n    if (btn) btn.disabled = false;\n  }\n}\n\n/* Панель мониторинга: счётчик. Карточка создаётся генериком (id mon-card-cookbook_counter). */\nfunction renderCookbookPanel(d) {\n  const el = document.querySelector(\"#mon-card-cookbook-counter .kv\");\n  if (!el) return;\n  d = d || {};\n  el.innerHTML = `<div><b>Счётчик</b>${esc(String(d.count ?? \"—\"))}</div>`;\n}\n\nwindow.BotUI.registerService('cookbook', 'cookbook');\nwindow.BotUI.registerView('cookbook', loadCookbook);\nwindow.BotUI.registerPanelRenderer('cookbook_counter', (el, p, d) => renderCookbookPanel(d));\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* cookbook — веб-кабинет модуля (modules/cookbook/web/view.js).</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">   Конвенция: load-функция тянет /api/&#x3C;mod>/*, вьюха регистрируется сама —</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">   в app.js ядра руками ничего не добавляется. esc()/escJs() — из app.js. */</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> loadCookbook</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> cbRefresh</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> cbRefresh</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#cb-counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Ошибка: \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> c</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.counter </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#cb-counter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"count = \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (c.count </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">??</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"?\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  } </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">finally</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> cbReset</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/reset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({}) });</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#cb-out\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.ok </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Сброшен\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка: \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">));</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> cbRefresh</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  } </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">finally</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> cbFact</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/fact\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#cb-out\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.ok </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.fact </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка: \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">));</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  } </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">finally</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> cbSetSave</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> key</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#cb-set-key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">trim</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> val</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#cb-set-val\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">trim</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">val) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Нужны ключ и значение\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/cookbook/settings\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ key, value: val }) });</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#cb-set-out\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.ok </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Сохранено: \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка: \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">));</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  } </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">finally</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* Панель мониторинга: счётчик. Карточка создаётся генериком (id mon-card-cookbook_counter). */</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> renderCookbookPanel</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">d</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> el</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#mon-card-cookbook-counter .kv\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">el) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  el.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;div>&#x3C;b>Счётчик&#x3C;/b>${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">String</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">count</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ??</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"—\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">))</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/div>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">window.BotUI.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">registerService</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'cookbook'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'cookbook'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">window.BotUI.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">registerView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'cookbook'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, loadCookbook);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">window.BotUI.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">registerPanelRenderer</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'cookbook_counter'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, (</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">el</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">d</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> renderCookbookPanel</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(d));</span></span>\n<span class=\"line\"></span></code></pre>"
        }
      ],
      "note": "Эталонный модуль для примера",
      "_extra": {
        "api": "",
        "events_emitted": "",
        "events_subscribed": "",
        "tg_commands": "",
        "ws_routes": "",
        "audit_allow": "",
        "config_schema": "config.schema.json",
        "env_example": ".env.example"
      }
    },
    {
      "name": "modsafe",
      "version": "0.4.1",
      "category": "base",
      "description": "Охрана модулей: аудит кода, хеши файлов, вкл/выкл (default-off для новых).",
      "requires": [],
      "rights": [],
      "api_methods": [],
      "web_routes": [],
      "commit": "2d7d04615b31be9f5bbd91d5d61671182164bf39",
      "updated": "2026-09-16 16:35:50 +0300",
      "integrity": "sha256:26f8f213dc3f7a345ee4cf820b750a5df01b7d6c1f7f43ee82874dd403cccc9a",
      "tree_url": "http://10.10.10.30:3000/euk0r/bot_modular/src/branch/main/modules/modsafe",
      "raw_url": "http://10.10.10.30:3000/euk0r/bot_modular/raw/branch/main/modules/modsafe/module.py",
      "files": [
        {
          "path": "README.md",
          "size": 3714,
          "content": "# modsafe `0.4.1`\n\nОхрана модулей: аудит кода, хеши файлов, вкл/выкл, секреты, маркет.\n\nТип: Модуль. Категория: `base`. Зависимостей нет.\n## Описание\nПривратник модульного ядра:\n- **аудит кода** — сканер ищет опасные паттерны с severity; легитимные исключения — в `audit_allow` манифеста; полный паспорт — через `inspect`;\n- **вкл/выкл** — галочки модулей (применяются рестартом; себя выключить нельзя);\n- **эталоны хешей** — `rescan` фиксирует «текущие файлы = доверенные»;\n- **секреты** — ключи `.env` по модулям без значений наружу;\n- **маркет** — загрузка zip'ом в стейджинг с аудитом до установки; установка — всегда выключенным.\n\nМаркет (`market`) — заглушка до запуска страницы: отдаёт `stub: true`, установка идёт через `stage` → `install`.\n## Возможности\n- `list` `{}` → все модули/транспорты со статусом\n- `audit_one` `{name}` → находки сканера по одному модулю\n- `set_enabled` `{name, enabled}` → вкл/выкл (применится после рестарта)\n- `rescan` `{name?}` → обновить эталон хешей\n- `stage` `{filename, content_type, body}` → принять архив в стейджинг + аудит\n- `staged` `{}` → список стейджинга\n- `install` `{name}` → установить из стейджинга (выключенным)\n- `market` `{}` → заглушка маркета (`stub: true`)\n- `delete` `{name}` → удалить модуль (только выключенный)\n- `pack` `{name, with_secrets}` → упаковать модуль в zip (b64)\n- `env_keys` `{name}` → ключи секретов без значений\n- `env_set` `{name, values}` → сохранить секреты\n- `inspect` `{name}` → полный паспорт модуля/транспорта\n- `orphans` `{}` → pip-пакеты без владельца (только показ)\n- `uninstall_orphans` `{packages}` → удалить сирот (лимит 20)\n## Права\n- `modules_manage` — Управление модулями: вкл/выкл, аудит, эталон хешей\n## Web\n- `GET /api/modsafe/modules` — список модулей\n- `POST /api/modsafe/enable` — вкл/выкл\n- `POST /api/modsafe/rescan` — обновить эталоны\n- `POST /api/modsafe/upload` — загрузка в стейджинг\n- `GET /api/modsafe/staged` — список стейджинга\n- `POST /api/modsafe/install` — установка (выключенным)\n- `GET /api/modsafe/market` — заглушка маркета\n- `POST /api/modsafe/delete` — удаление\n- `GET /api/modsafe/pack` — упаковка в zip\n- `GET/POST /api/modsafe/env` — секреты модулей\n- `GET /api/modsafe/inspect` — паспорт модуля\n- `GET/POST /api/modsafe/orphans` — пакеты-сироты\n- кабинет «Модули»\n## Конфигурация (`config.schema.json`)\n- обязательных нет; опциональные (дефолты):\n  - `MODSAFE_MAX_UPLOAD_MB` = `5` — лимит архива, МБ\n  - `MODSAFE_MARKET_URL` = `` (не задан) — адрес будущего маркета\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\"># modsafe </span><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">`0.4.1`</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Охрана модулей: аудит кода, хеши файлов, вкл/выкл, секреты, маркет.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Тип: Модуль. Категория: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`base`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">. Зависимостей нет.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Описание</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Привратник модульного ядра:</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-light-font-weight:bold;--shiki-dark:#E1E4E8;--shiki-dark-font-weight:bold\"> **аудит кода**</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — сканер ищет опасные паттерны с severity; легитимные исключения — в </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`audit_allow`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> манифеста; полный паспорт — через </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`inspect`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-light-font-weight:bold;--shiki-dark:#E1E4E8;--shiki-dark-font-weight:bold\"> **вкл/выкл**</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — галочки модулей (применяются рестартом; себя выключить нельзя);</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-light-font-weight:bold;--shiki-dark:#E1E4E8;--shiki-dark-font-weight:bold\"> **эталоны хешей**</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`rescan`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> фиксирует «текущие файлы = доверенные»;</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-light-font-weight:bold;--shiki-dark:#E1E4E8;--shiki-dark-font-weight:bold\"> **секреты**</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — ключи </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`.env`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> по модулям без значений наружу;</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-light-font-weight:bold;--shiki-dark:#E1E4E8;--shiki-dark-font-weight:bold\"> **маркет**</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — загрузка zip'ом в стейджинг с аудитом до установки; установка — всегда выключенным.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Маркет (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`market`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) — заглушка до запуска страницы: отдаёт </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`stub: true`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, установка идёт через </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`stage`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`install`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Возможности</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `list`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → все модули/транспорты со статусом</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `audit_one`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{name}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → находки сканера по одному модулю</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `set_enabled`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{name, enabled}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → вкл/выкл (применится после рестарта)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `rescan`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{name?}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → обновить эталон хешей</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `stage`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{filename, content_type, body}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → принять архив в стейджинг + аудит</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `staged`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → список стейджинга</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `install`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{name}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → установить из стейджинга (выключенным)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `market`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → заглушка маркета (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`stub: true`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `delete`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{name}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → удалить модуль (только выключенный)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `pack`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{name, with_secrets}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → упаковать модуль в zip (b64)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `env_keys`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{name}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → ключи секретов без значений</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `env_set`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{name, values}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → сохранить секреты</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `inspect`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{name}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → полный паспорт модуля/транспорта</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `orphans`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → pip-пакеты без владельца (только показ)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `uninstall_orphans`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{packages}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → удалить сирот (лимит 20)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Права</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `modules_manage`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — Управление модулями: вкл/выкл, аудит, эталон хешей</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Web</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `GET /api/modsafe/modules`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — список модулей</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `POST /api/modsafe/enable`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — вкл/выкл</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `POST /api/modsafe/rescan`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — обновить эталоны</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `POST /api/modsafe/upload`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — загрузка в стейджинг</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `GET /api/modsafe/staged`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — список стейджинга</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `POST /api/modsafe/install`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — установка (выключенным)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `GET /api/modsafe/market`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — заглушка маркета</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `POST /api/modsafe/delete`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — удаление</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `GET /api/modsafe/pack`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — упаковка в zip</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `GET/POST /api/modsafe/env`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — секреты модулей</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `GET /api/modsafe/inspect`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — паспорт модуля</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `GET/POST /api/modsafe/orphans`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — пакеты-сироты</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> кабинет «Модули»</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Конфигурация (</span><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">`config.schema.json`</span><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> обязательных нет; опциональные (дефолты):</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">  -</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `MODSAFE_MAX_UPLOAD_MB`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> = </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`5`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — лимит архива, МБ</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">  -</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `MODSAFE_MARKET_URL`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> = `` (не задан) — адрес будущего маркета</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "config.schema.json",
          "size": 105,
          "content": "{\n  \"required\": [],\n  \"optional\": {\n    \"MODSAFE_MAX_UPLOAD_MB\": \"5\",\n    \"MODSAFE_MARKET_URL\": \"\"\n  }\n}\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">{</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"required\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: [],</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"optional\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"MODSAFE_MAX_UPLOAD_MB\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"5\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"MODSAFE_MARKET_URL\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "installer.py",
          "size": 25410,
          "content": "\"\"\"modsafe.installer — загрузка модулей архивом: стейджинг, проверки, установка.\n\nПайплайн (всё под правом modules_manage):\n  upload (POST multipart) -> data/modstaging/<name>/ + аудит сканером;\n  review (🔍 в таблице) — человек смотрит находки;\n  install -> modules/<name>/, запись modstate ВЫКЛЮЧЕННЫМ, эталон хешей.\nВключение — вручную (modsafe.enable) + рестарт. Маркет (список со страницы)\n— заглушка до запуска страницы (modsafe.market).\n\nЗащита: только .zip/.tar.gz, лимиты размера/числа файлов, запрет ..,\nабсолютных путей, симлинков и бинарных расширений, имя ^[a-z][a-z0-9_]*$,\nманифест с обязательными полями, отказ при существующем каталоге.\n\"\"\"\nimport io\nimport logging\nimport os\nimport re\nimport shutil\nimport tarfile\nimport zipfile\n\nlogger = logging.getLogger(__name__)\n\n_NAME_RX = re.compile(r\"^[a-z][a-z0-9_]*$\")\n_BLOCK_EXT = {\".so\", \".dll\", \".exe\", \".bin\", \".apk\"}\n\n\ndef _limits(config):\n    def _int(key, default):\n        try:\n            return int((config.get(key, default) if config else default) or default)\n        except (ValueError, TypeError, AttributeError):\n            return default\n    max_mb = _int(\"MODSAFE_MAX_UPLOAD_MB\", 5)\n    return max_mb * 1024 * 1024, 200\n\n\ndef _read_manifest(d):\n    import json\n    for fn in (\"manifest.yaml\", \"manifest.yml\", \"manifest.json\"):\n        p = os.path.join(d, fn)\n        if os.path.isfile(p):\n            with open(p, \"r\", encoding=\"utf-8\") as f:\n                if fn.endswith(\".json\"):\n                    data = json.load(f)\n                else:\n                    import yaml\n                    data = yaml.safe_load(f)\n            if not isinstance(data, dict):\n                raise ValueError(\"манифест %s должен быть объектом\" % fn)\n            return data\n    raise ValueError(\"в архиве нет manifest.yaml/json\")\n\n\ndef _check_manifest(data, dirname):\n    from core.base_module import MANIFEST_REQUIRED\n    for field in MANIFEST_REQUIRED:\n        if not data.get(field):\n            raise ValueError(\"манифест: нет обязательного поля %s\" % field)\n    if data.get(\"name\") != dirname:\n        raise ValueError(\"манифест name=%s не совпадает с именем %s\"\n                         % (data.get(\"name\"), dirname))\n    if not _NAME_RX.match(data.get(\"name\") or \"\"):\n        raise ValueError(\"недопустимое имя модуля (латиница, цифры, _)\")\n\n\ndef _safe_members_zip(zf):\n    out = []\n    for info in zf.infolist():\n        if info.is_dir():\n            continue\n        name = info.filename\n        out.append((name, info.file_size, False))\n    return out\n\n\ndef _safe_members_tar(tf):\n    out = []\n    for m in tf.getmembers():\n        if m.isdir():\n            continue\n        out.append((m.name, m.size, m.issym() or m.islnk()))\n    return out\n\n\ndef _validate_members(members, max_bytes):\n    total = 0\n    if len(members) > 200 or not members:\n        raise ValueError(\"в архиве %d файлов (пусто или больше 200)\" % len(members))\n    for name, size, is_link in members:\n        if is_link:\n            raise ValueError(\"симлинки запрещены: %s\" % name)\n        if not name or name.startswith(\"/\") or name.startswith(\"\\\\\"):\n            raise ValueError(\"абсолютный путь: %s\" % name)\n        parts = [p for p in name.replace(\"\\\\\", \"/\").split(\"/\") if p not in (\"\", \".\")]\n        if not parts or any(p == \"..\" for p in name.replace(\"\\\\\", \"/\").split(\"/\")):\n            raise ValueError(\"выход за каталог: %s\" % name)\n        if os.path.splitext(parts[-1])[1].lower() in _BLOCK_EXT:\n            raise ValueError(\"бинарные файлы запрещены: %s\" % name)\n        total += size or 0\n        if total > max_bytes:\n            raise ValueError(\"архив больше лимита\")\n    return total\n\n\ndef stage_upload(data_dir, modules_dir, config, filename, body):\n    \"\"\"Принять архив, разложить в стейджинг, проверить, просканировать.\"\"\"\n    from core import audit as _audit\n    max_bytes, _ = _limits(config)\n    filename = (filename or \"\").strip()\n    body = body or b\"\"\n    if not body:\n        raise ValueError(\"пустой файл\")\n    if len(body) > max_bytes:\n        raise ValueError(\"файл больше лимита %d МБ\" % (max_bytes // 1048576))\n    low = filename.lower()\n    kind = \"zip\" if low.endswith(\".zip\") else (\n        \"tar\" if low.endswith((\".tar.gz\", \".tgz\", \".tar\")) else \"\")\n    if not kind:\n        raise ValueError(\"только .zip / .tar.gz\")\n    staging = os.path.join(data_dir, \"modstaging\")\n    os.makedirs(staging, exist_ok=True)\n    tmp = os.path.join(staging, \"_incoming\")\n    if os.path.isdir(tmp):\n        shutil.rmtree(tmp)\n    os.makedirs(tmp)\n    try:\n        if kind == \"zip\":\n            with zipfile.ZipFile(io.BytesIO(body)) as zf:\n                members = _safe_members_zip(zf)\n                _validate_members(members, max_bytes)\n                for name, _, _ in members:\n                    dest = os.path.join(tmp, *name.replace(\"\\\\\", \"/\").split(\"/\"))\n                    if dest.startswith(tmp + os.sep):\n                        os.makedirs(os.path.dirname(dest), exist_ok=True)\n                        with zf.open(name) as src, open(dest, \"wb\") as dst:\n                            shutil.copyfileobj(src, dst, 65536)\n        else:\n            with tarfile.open(fileobj=io.BytesIO(body), mode=\"r:*\") as tf:\n                members = _safe_members_tar(tf)\n                _validate_members(members, max_bytes)\n                for m in tf.getmembers():\n                    if m.isdir():\n                        continue\n                    dest = os.path.join(tmp, *m.name.replace(\"\\\\\", \"/\").split(\"/\"))\n                    if not dest.startswith(tmp + os.sep):\n                        raise ValueError(\"выход за каталог: %s\" % m.name)\n                    os.makedirs(os.path.dirname(dest), exist_ok=True)\n                    src = tf.extractfile(m)\n                    if src is None:\n                        raise ValueError(\"не читается: %s\" % m.name)\n                    with src, open(dest, \"wb\") as dst:\n                        shutil.copyfileobj(src, dst, 65536)\n    except (zipfile.BadZipFile, tarfile.TarError) as e:\n        raise ValueError(\"битый архив: %s\" % e)\n    # unwrap: единственный корневой каталог -> его содержимое\n    top = [e for e in sorted(os.listdir(tmp))\n           if os.path.isdir(os.path.join(tmp, e))]\n    files_top = [e for e in sorted(os.listdir(tmp))\n                 if os.path.isfile(os.path.join(tmp, e))]\n    root = tmp\n    if len(top) == 1 and not files_top:\n        root = os.path.join(tmp, top[0])\n    manifest = _read_manifest(root)\n    name = manifest.get(\"name\")\n    _check_manifest(manifest, name)\n    if os.path.exists(os.path.join(modules_dir, name)):\n        shutil.rmtree(tmp)\n        raise ValueError(\"модуль %s уже установлен\" % name)\n    dest = os.path.join(staging, name)\n    if os.path.isdir(dest):\n        shutil.rmtree(dest)\n    if root == tmp:\n        os.rename(tmp, dest)\n    else:\n        shutil.move(root, dest)\n        shutil.rmtree(tmp)\n    findings = _audit.scan_module(dest, manifest.get(\"audit_allow\") or [])\n    summary = _audit.summarize(findings)\n    logger.info(\"modsafe: застейджен %s v%s (%s)\",\n                name, manifest.get(\"version\"), summary)\n    return {\"ok\": True, \"name\": name, \"version\": manifest.get(\"version\") or \"\",\n            \"description\": manifest.get(\"description\") or \"\",\n            \"summary\": summary, \"findings\": findings[:200],\n            \"staged\": True}\n\n\ndef list_staged(data_dir):\n    \"\"\"Стейджинг: [{name, version, description}] с валидным манифестом.\"\"\"\n    staging = os.path.join(data_dir, \"modstaging\")\n    out = []\n    if not os.path.isdir(staging):\n        return out\n    for entry in sorted(os.listdir(staging)):\n        if entry.startswith(\"_\"):\n            continue\n        d = os.path.join(staging, entry)\n        if not os.path.isdir(d):\n            continue\n        try:\n            m = _read_manifest(d)\n            out.append({\"name\": entry, \"version\": m.get(\"version\") or \"\",\n                        \"description\": m.get(\"description\") or \"\"})\n        except Exception:  # noqa: BLE001\n            out.append({\"name\": entry, \"version\": \"\", \"description\": \"битый стейджинг\"})\n    return out\n\n\ndef install_staged(data_dir, modules_dir, name):\n    \"\"\"Переместить стейджинг в modules/, выключенным, с эталоном хешей.\"\"\"\n    from core import audit as _audit\n    from core import modstate as _ms\n    if not _NAME_RX.match(name or \"\"):\n        raise ValueError(\"недопустимое имя\")\n    src = os.path.join(data_dir, \"modstaging\", name)\n    dest = os.path.join(modules_dir, name)\n    if not os.path.isdir(src):\n        raise ValueError(\"в стейджинге нет %s\" % name)\n    if os.path.exists(dest):\n        raise ValueError(\"модуль %s уже установлен\" % name)\n    manifest = _read_manifest(src)\n    _check_manifest(manifest, name)\n    shutil.move(src, dest)\n    # запись modstate: известен, ВЫКЛЮЧЕН; эталон = текущие файлы\n    state = _ms.load(data_dir)\n    mods = state.setdefault(\"modules\", {})\n    rec = mods.get(name)\n    if not isinstance(rec, dict):\n        rec = {\"enabled\": False, \"known\": True, \"hashes\": {},\n               \"hashes_at\": \"\", \"audit\": {}, \"audited_at\": \"\",\n               \"missing\": False}\n        mods[name] = rec\n    rec[\"enabled\"] = False\n    rec[\"known\"] = True\n    rec[\"missing\"] = False\n    _ms.save(data_dir, state)\n    _ms.update_hashes(data_dir, name, _audit.hash_module(dest))\n    logger.info(\"modsafe: установлен %s (выключен)\", name)\n    return {\"ok\": True, \"name\": name, \"enabled\": False, \"apply\": \"restart\"}\n\n\n#: Файлы, никогда не попадающие в архив без секретов.\n_SECRET_NAMES = {\".env\"}\n_SECRET_EXT = {\".session\"}\n\n#: Мусор, не попадающий в архив никогда.\n_SKIP_DIRS = {\"__pycache__\", \".git\", \"node_modules\"}\n_SKIP_EXT = {\".pyc\", \".pyo\"}\n\n\ndef _module_files(mod_dir, with_secrets):\n    out = []\n    for root, dirs, files in os.walk(mod_dir):\n        dirs[:] = sorted(d for d in dirs if d not in _SKIP_DIRS)\n        for fn in sorted(files):\n            if os.path.splitext(fn)[1] in _SKIP_EXT:\n                continue\n            if not with_secrets and (\n                    fn in _SECRET_NAMES\n                    or os.path.splitext(fn)[1] in _SECRET_EXT):\n                continue\n            p = os.path.join(root, fn)\n            if os.path.isfile(p) and not os.path.islink(p):\n                out.append(p)\n    return out\n\n\ndef pack_module(modules_dir, name, with_secrets=False):\n    \"\"\"Упаковать модуль в zip. Возвращает (filename, bytes).\n\n    with_secrets=False (дефолт): без .env/*.session — безопасно делиться.\n    \"\"\"\n    if not _NAME_RX.match(name or \"\"):\n        raise ValueError(\"недопустимое имя\")\n    if name == \"modsafe\":\n        raise ValueError(\"modsafe не упаковывается\")\n    mod_dir = os.path.join(modules_dir, name)\n    if not os.path.isdir(mod_dir):\n        raise ValueError(\"модуль %s не найден\" % name)\n    manifest = _read_manifest(mod_dir)\n    _check_manifest(manifest, name)\n    files = _module_files(mod_dir, with_secrets)\n    if not files:\n        raise ValueError(\"в модуле нет файлов для упаковки\")\n    buf = io.BytesIO()\n    with zipfile.ZipFile(buf, \"w\", zipfile.ZIP_DEFLATED) as zf:\n        for p in files:\n            rel = os.path.relpath(p, mod_dir).replace(os.sep, \"/\")\n            zf.write(p, os.path.join(name, rel))\n    data = buf.getvalue()\n    logger.info(\"modsafe: упакован %s (%d файлов, %d байт, secrets=%s)\",\n                name, len(files), len(data), bool(with_secrets))\n    return \"%s%s.zip\" % (name, \"-secrets\" if with_secrets else \"\"), data\n\n\ndef delete_module(data_dir, modules_dir, name, must_be_disabled=True):\n    \"\"\"Удалить модуль: каталог + запись modstate + стейджинг.\n\n    Только выключенные (сначала modsafe.enable=false + рестарт).\n    modsafe и транспорты удалять нельзя.\n    \"\"\"\n    from core import modstate as _ms\n    if not _NAME_RX.match(name or \"\"):\n        raise ValueError(\"недопустимое имя\")\n    if name == \"modsafe\":\n        raise ValueError(\"modsafe удалять нельзя\")\n    mod_dir = os.path.join(modules_dir, name)\n    if not os.path.isdir(mod_dir):\n        raise ValueError(\"модуль %s не найден\" % name)\n    state = _ms.load(data_dir)\n    rec = (state.get(\"modules\") or {}).get(name) or {}\n    if must_be_disabled and rec.get(\"enabled\"):\n        raise ValueError(\"модуль %s включён: сначала выключите и перезапустите\" % name)\n    shutil.rmtree(mod_dir)\n    if name in (state.get(\"modules\") or {}):\n        del state[\"modules\"][name]\n        _ms.save(data_dir, state)\n    staging = os.path.join(data_dir, \"modstaging\", name)\n    if os.path.isdir(staging):\n        shutil.rmtree(staging)\n    logger.info(\"modsafe: удалён %s\", name)\n    return {\"ok\": True, \"name\": name}\n\n\ndef _env_path(modules_dir, name, env_dir=None):\n    \"\"\"Путь .env модуля: data/env/<name>.env; legacy modules/<name>/.env\n    учитывается только для флага has_env (миграция — через install.sh).\"\"\"\n    main = os.path.join(env_dir, name + \".env\") if env_dir else None\n    legacy = os.path.join(modules_dir, name, \".env\")\n    has_legacy = os.path.isfile(legacy)\n    if main and os.path.isfile(main):\n        return main, has_legacy\n    if has_legacy:\n        return legacy, False\n    # нет нигде — писать в основной (если есть куда), иначе legacy\n    return (main or legacy), False\n\n\ndef env_keys(modules_dir, name, env_dir=None):\n    \"\"\"Ключи секретов модуля: схема + ключи из .env (БЕЗ значений).\n\n    Динамические секреты (auth_ref точек outbound и т.п.) нельзя перечислить\n    в схеме — их имена берутся из самого .env (dynamic: true).\n    \"\"\"\n    import json\n    if not _NAME_RX.match(name or \"\"):\n        raise ValueError(\"недопустимое имя\")\n    mod_dir = os.path.join(modules_dir, name)\n    if not os.path.isdir(mod_dir):\n        raise ValueError(\"модуль %s не найден\" % name)\n    schema = {}\n    sp = os.path.join(mod_dir, \"config.schema.json\")\n    if os.path.isfile(sp):\n        try:\n            schema = json.load(open(sp, encoding=\"utf-8\"))\n        except (OSError, ValueError):\n            pass\n    required = list(schema.get(\"required\") or [])\n    defaults = dict(schema.get(\"optional\") or {})\n    have = set()\n    ep, legacy = _env_path(modules_dir, name, env_dir)\n    if os.path.isfile(ep):\n        try:\n            with open(ep, encoding=\"utf-8\") as f:\n                for line in f:\n                    line = line.strip()\n                    if line and not line.startswith(\"#\") and \"=\" in line:\n                        have.add(line.split(\"=\", 1)[0].strip())\n        except OSError:\n            pass\n    keys = [{\"key\": k, \"required\": k in required, \"schema\": True,\n             \"set\": k in have, \"default\": str(defaults.get(k, \"\"))}\n            for k in required + [k for k in defaults if k not in required]]\n    seen = {k[\"key\"] for k in keys}\n    for k in sorted(have - seen):\n        keys.append({\"key\": k, \"required\": False, \"schema\": False,\n                     \"set\": True, \"default\": \"\"})\n    out = {\"name\": name, \"keys\": keys,\n           \"has_env\": os.path.isfile(ep)}\n    if legacy:\n        out[\"legacy_env\"] = True\n    return out\n\n\ndef env_set(modules_dir, name, values, env_dir=None):\n    \"\"\"Сохранить секреты модуля (.env 0600). Пустые значения — оставить как есть.\n\n    Ключ допустим, если он в схеме (required+optional) ЛИБО похож на имя\n    переменной окружения ([A-Z][A-Z0-9_], до 64): динамические секреты\n    (auth_ref и т.п.) в схеме не перечислить. Остальное — отказ.\n    \"\"\"\n    import json\n    import re as _re\n    if not _NAME_RX.match(name or \"\"):\n        raise ValueError(\"недопустимое имя\")\n    mod_dir = os.path.join(modules_dir, name)\n    if not os.path.isdir(mod_dir):\n        raise ValueError(\"модуль %s не найден\" % name)\n    schema = {}\n    sp = os.path.join(mod_dir, \"config.schema.json\")\n    if os.path.isfile(sp):\n        try:\n            schema = json.load(open(sp, encoding=\"utf-8\"))\n        except (OSError, ValueError):\n            pass\n    allowed = set(schema.get(\"required\") or []) | set((schema.get(\"optional\") or {}))\n    if not allowed:\n        raise ValueError(\"у модуля %s нет схемы секретов\" % name)\n    values = values or {}\n    unknown = sorted(k for k in values\n                     if k not in allowed\n                     and not _re.fullmatch(r\"[A-Z][A-Z0-9_]{0,63}\", k or \"\"))\n    if unknown:\n        raise ValueError(\"недопустимые ключи: %s\" % \", \".join(unknown))\n    ep, _legacy = _env_path(modules_dir, name, env_dir)\n    current = {}\n    order = []\n    if os.path.isfile(ep):\n        try:\n            with open(ep, encoding=\"utf-8\") as f:\n                for line in f:\n                    s = line.strip()\n                    if s and not s.startswith(\"#\") and \"=\" in s:\n                        k, v = s.split(\"=\", 1)\n                        k = k.strip()\n                        if k not in current:\n                            order.append(k)\n                        current[k] = v.strip()\n        except OSError:\n            pass\n    changed = []\n    for k, v in values.items():\n        v = (v or \"\").strip()\n        if not v:\n            continue  # пустое = не менять\n        if current.get(k) != v:\n            current[k] = v\n            changed.append(k)\n        if k not in order:\n            order.append(k)\n    if changed:\n        parent = os.path.dirname(ep)\n        if parent:\n            os.makedirs(parent, exist_ok=True)\n        tmp = ep + \".tmp\"\n        with open(tmp, \"w\", encoding=\"utf-8\") as f:\n            for k in order:\n                if k in current:\n                    f.write(\"%s=%s\\n\" % (k, current[k]))\n        os.chmod(tmp, 0o600)\n        os.replace(tmp, ep)\n    logger.info(\"modsafe: секреты %s: изменено %d\", name, len(changed))\n    return {\"ok\": True, \"name\": name, \"changed\": changed}\n\n#: Базовые пакеты venv — никогда не сироты.\n_BASE_PKGS = frozenset({\"pip\", \"setuptools\", \"wheel\"})\n\n\ndef _norm_pkg(name):\n    import re as _re\n    return _re.sub(r\"[-_.]+\", \"-\", (name or \"\").lower()).strip(\"-\")\n\n\ndef _req_names(path):\n    \"\"\"Имена пакетов из requirements.txt (без версий/маршрутов).\"\"\"\n    import re as _re\n    out = set()\n    try:\n        with open(path, \"r\", encoding=\"utf-8\", errors=\"replace\") as f:\n            for line in f:\n                line = line.strip()\n                if not line or line.startswith(\"#\"):\n                    continue\n                line = line.split(\";\", 1)[0].strip()\n                m = _re.match(r\"^([A-Za-z0-9_.-]+)\", line)\n                if m:\n                    out.add(_norm_pkg(m.group(1)))\n    except OSError:\n        pass\n    return out\n\n\ndef pip_orphans(modules_dir, transports_dir, root_dir):\n    \"\"\"Пакеты в venv, которых не требует никто (только показ, без удаления).\n\n    required = union requirements.txt корня + транспортов + УСТАНОВЛЕННЫХ\n    модулей (удалённого модуля уже нет в дереве — его пакеты и всплывут).\n    Замыкание по зависимостям зависимостей — через Requires-Dist.\n    Возвращает {orphans: [{name, version}], required_n, installed_n}.\n    \"\"\"\n    try:\n        from importlib import metadata as _md\n    except ImportError:\n        return {\"orphans\": [], \"required_n\": 0, \"installed_n\": 0,\n                \"error\": \"нет importlib.metadata\"}\n    roots = set()\n    rp = os.path.join(root_dir, \"requirements.txt\")\n    if os.path.isfile(rp):\n        roots |= _req_names(rp)\n    for base in (modules_dir, transports_dir):\n        try:\n            entries = sorted(os.listdir(base))\n        except OSError:\n            continue\n        for entry in entries:\n            fp = os.path.join(base, entry, \"requirements.txt\")\n            if os.path.isfile(fp):\n                roots |= _req_names(fp)\n    installed = {}\n    for dist in _md.distributions():\n        try:\n            n = _norm_pkg(dist.metadata[\"Name\"] or \"\")\n        except Exception:  # noqa: BLE001\n            continue\n        if n:\n            installed.setdefault(n, str(dist.version or \"\"))\n    # замыкание: требуемое + всё, что тянут их Requires-Dist\n    keep = set(roots) | set(_BASE_PKGS)\n    queue = sorted(keep)\n    while queue:\n        cur = queue.pop()\n        try:\n            reqs = _md.requires(cur) or []\n        except Exception:  # noqa: BLE001\n            continue\n        for r in reqs:\n            m = _norm_pkg(r.split(\";\", 1)[0].strip().split()[0]\n                          .split(\"[\")[0].split(\"(\")[0].rstrip(\",\"))\n            import re as _re\n            mm = _re.match(r\"^([A-Za-z0-9_.-]+)\", m)\n            name = _norm_pkg(mm.group(1)) if mm else \"\"\n            if name and name not in keep:\n                keep.add(name)\n                queue.append(name)\n    orphans = [{\"name\": n, \"version\": installed[n]}\n               for n in sorted(installed) if n not in keep]\n    return {\"orphans\": orphans, \"required_n\": len(keep),\n            \"installed_n\": len(installed)}\n\n\ndef pip_uninstall_orphans(modules_dir, transports_dir, root_dir, packages):\n    \"\"\"Удалить пакеты-сироты тем же интерпретатором (pip uninstall -y).\n\n    Безопасность: каждый пакет перепроверяется на сиротство ПЕРЕД удалением\n    (свежее замыкание, клиентскому списку не доверяем); базовые пакеты\n    (pip/setuptools/wheel) и пустые имена — отказ; лимит 20 за вызов.\n    Возвращает {removed: [...], skipped: [{name, reason}], output}.\n    \"\"\"\n    import re as _re\n    import subprocess as _sp\n    import sys as _sys\n    wanted = []\n    for p in packages or []:\n        n = _norm_pkg(p or \"\")\n        if n and n not in wanted:\n            wanted.append(n)\n    if not wanted:\n        raise ValueError(\"пустой список пакетов\")\n    if len(wanted) > 20:\n        raise ValueError(\"больше 20 за раз нельзя\")\n    fresh = pip_orphans(modules_dir, transports_dir, root_dir)\n    still = {o[\"name\"] for o in fresh.get(\"orphans\") or []}\n    removed, skipped, out_lines = [], [], []\n    for n in wanted:\n        if n in _BASE_PKGS:\n            skipped.append({\"name\": n, \"reason\": \"базовый пакет venv\"})\n            continue\n        if n not in still:\n            skipped.append({\"name\": n, \"reason\": \"уже требуется (не сирота)\"})\n            continue\n        if not _re.fullmatch(r\"[a-z0-9][a-z0-9_.-]*\", n):\n            skipped.append({\"name\": n, \"reason\": \"плохое имя\"})\n            continue\n        try:\n            r = _sp.run([_sys.executable, \"-m\", \"pip\", \"uninstall\",\n                         \"-y\", n],\n                        capture_output=True, text=True, timeout=120)\n            tail = ((r.stdout or \"\") + \"\\n\" + (r.stderr or \"\")).strip().splitlines()\n            out_lines += [\"== %s (rc=%d)\" % (n, r.returncode)]\n            out_lines += [l[:200] for l in tail[-4:]]\n            if r.returncode == 0:\n                removed.append(n)\n            else:\n                skipped.append({\"name\": n,\n                                \"reason\": \"pip rc=%d\" % r.returncode})\n        except Exception as e:  # noqa: BLE001\n            skipped.append({\"name\": n, \"reason\": \"%s: %s\" % (\n                type(e).__name__, e)})\n    logger.info(\"modsafe: pip-сироты удалены: %s, пропущены: %s\",\n                removed, [s[\"name\"] for s in skipped])\n    return {\"ok\": True, \"removed\": removed, \"skipped\": skipped,\n            \"output\": \"\\n\".join(out_lines)[:4000]}\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"modsafe.installer — загрузка модулей архивом: стейджинг, проверки, установка.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Пайплайн (всё под правом modules_manage):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  upload (POST multipart) -> data/modstaging/&#x3C;name>/ + аудит сканером;</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  review (🔍 в таблице) — человек смотрит находки;</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">  install -> modules/&#x3C;name>/, запись modstate ВЫКЛЮЧЕННЫМ, эталон хешей.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Включение — вручную (modsafe.enable) + рестарт. Маркет (список со страницы)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">— заглушка до запуска страницы (modsafe.market).</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Защита: только .zip/.tar.gz, лимиты размера/числа файлов, запрет ..,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">абсолютных путей, симлинков и бинарных расширений, имя ^[a-z][a-z0-9_]*$,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">манифест с обязательными полями, отказ при существующем каталоге.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> io</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> shutil</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tarfile</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> zipfile</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">logger </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging.getLogger(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__name__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_NAME_RX</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re.compile(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">^[a-z][a-z0-9_]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">$</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_BLOCK_EXT</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".so\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".dll\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".exe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".bin\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".apk\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _limits</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(config):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(key, default):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((config.get(key, default) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> default) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> default)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TypeError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">AttributeError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> default</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    max_mb </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _int(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MODSAFE_MAX_UPLOAD_MB\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> max_mb </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1024</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> *</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1024</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">200</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _read_manifest</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(d):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fn </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manifest.yaml\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manifest.yml\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manifest.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        p </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(d, fn)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isfile(p):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(p, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"r\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fn.endswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json.load(f)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> yaml</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> yaml.safe_load(f)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"манифест </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> должен быть объектом\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fn)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"в архиве нет manifest.yaml/json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _check_manifest</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data, dirname):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.base_module </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> MANIFEST_REQUIRED</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> field </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> MANIFEST_REQUIRED</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.get(field):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"манифест: нет обязательного поля </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> field)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dirname:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"манифест name=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> не совпадает с именем </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                         %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (data.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), dirname))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _NAME_RX</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.match(data.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"недопустимое имя модуля (латиница, цифры, _)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _safe_members_zip</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(zf):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    out </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> info </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> zf.infolist():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> info.is_dir():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> info.filename</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out.append((name, info.file_size, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> out</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _safe_members_tar</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(tf):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    out </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tf.getmembers():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m.isdir():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out.append((m.name, m.size, m.issym() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m.islnk()))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> out</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _validate_members</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(members, max_bytes):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    total </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(members) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 200</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> or</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> members:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"в архиве </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> файлов (пусто или больше 200)\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(members))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name, size, is_link </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> members:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> is_link:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"симлинки запрещены: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\\</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"абсолютный путь: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        parts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [p </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> p </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name.replace(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\\</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> p </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> parts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> any</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(p </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"..\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> p </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name.replace(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\\</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"выход за каталог: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.splitext(parts[</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].lower() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _BLOCK_EXT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"бинарные файлы запрещены: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        total </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> size </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> total </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> max_bytes:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"архив больше лимита\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> total</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> stage_upload</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data_dir, modules_dir, config, filename, body):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Принять архив, разложить в стейджинг, проверить, просканировать.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> audit </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _audit</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    max_bytes, _ </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _limits(config)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    filename </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (filename </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> b</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> body:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"пустой файл\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(body) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> max_bytes:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"файл больше лимита </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> МБ\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (max_bytes </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">//</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1048576</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    low </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> filename.lower()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    kind </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"zip\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> low.endswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".zip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"tar\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> low.endswith((</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".tar.gz\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".tgz\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".tar\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> kind:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"только .zip / .tar.gz\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    staging </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(data_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modstaging\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    os.makedirs(staging, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">exist_ok</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    tmp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(staging, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"_incoming\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isdir(tmp):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        shutil.rmtree(tmp)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    os.makedirs(tmp)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> kind </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"zip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            with</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> zipfile.ZipFile(io.BytesIO(body)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> zf:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                members </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _safe_members_zip(zf)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                _validate_members(members, max_bytes)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name, _, _ </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> members:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    dest </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(tmp, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name.replace(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\\</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dest.startswith(tmp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.sep):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        os.makedirs(os.path.dirname(dest), </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">exist_ok</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        with</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> zf.open(name) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> src, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(dest, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"wb\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dst:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                            shutil.copyfileobj(src, dst, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">65536</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            with</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tarfile.open(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">fileobj</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">io.BytesIO(body), </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">mode</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"r:*\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tf:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                members </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _safe_members_tar(tf)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                _validate_members(members, max_bytes)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tf.getmembers():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m.isdir():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    dest </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(tmp, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m.name.replace(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\\</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dest.startswith(tmp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.sep):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"выход за каталог: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m.name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    os.makedirs(os.path.dirname(dest), </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">exist_ok</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    src </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tf.extractfile(m)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> src </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"не читается: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m.name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    with</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> src, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(dest, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"wb\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dst:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        shutil.copyfileobj(src, dst, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">65536</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (zipfile.BadZipFile, tarfile.TarError) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"битый архив: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # unwrap: единственный корневой каталог -> его содержимое</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    top </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [e </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(os.listdir(tmp))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">           if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isdir(os.path.join(tmp, e))]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    files_top </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [e </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(os.listdir(tmp))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                 if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isfile(os.path.join(tmp, e))]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    root </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tmp</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(top) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> and</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> files_top:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        root </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(tmp, top[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    manifest </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _read_manifest(root)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> manifest.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _check_manifest(manifest, name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.exists(os.path.join(modules_dir, name)):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        shutil.rmtree(tmp)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"модуль </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> уже установлен\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    dest </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(staging, name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isdir(dest):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        shutil.rmtree(dest)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> root </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tmp:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        os.rename(tmp, dest)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        shutil.move(root, dest)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        shutil.rmtree(tmp)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    findings </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _audit.scan_module(dest, manifest.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"audit_allow\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    summary </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _audit.summarize(findings)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    logger.info(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe: застейджен </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> v</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                name, manifest.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"version\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), summary)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: name, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"version\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: manifest.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"version\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"description\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: manifest.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"description\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"summary\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: summary, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"findings\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: findings[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">200</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"staged\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> list_staged</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data_dir):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Стейджинг: [{name, version, description}] с валидным манифестом.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    staging </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(data_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modstaging\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    out </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isdir(staging):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> out</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entry </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(os.listdir(staging)):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entry.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"_\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(staging, entry)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isdir(d):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _read_manifest(d)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out.append({</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: entry, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"version\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"version\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                        \"description\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"description\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">})</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out.append({</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: entry, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"version\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"description\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"битый стейджинг\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">})</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> out</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> install_staged</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data_dir, modules_dir, name):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Переместить стейджинг в modules/, выключенным, с эталоном хешей.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> audit </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _audit</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> modstate </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _ms</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _NAME_RX</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.match(name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"недопустимое имя\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    src </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(data_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modstaging\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    dest </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(modules_dir, name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isdir(src):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"в стейджинге нет </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.exists(dest):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"модуль </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> уже установлен\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    manifest </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _read_manifest(src)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _check_manifest(manifest, name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    shutil.move(src, dest)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # запись modstate: известен, ВЫКЛЮЧЕН; эталон = текущие файлы</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    state </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _ms.load(data_dir)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    mods </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state.setdefault(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {})</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    rec </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mods.get(name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(rec, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        rec </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"enabled\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"known\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hashes\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {},</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">               \"hashes_at\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"audit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {}, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"audited_at\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">               \"missing\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mods[name] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rec</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    rec[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"enabled\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    rec[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"known\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    rec[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"missing\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _ms.save(data_dir, state)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _ms.update_hashes(data_dir, name, _audit.hash_module(dest))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    logger.info(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe: установлен </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (выключен)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: name, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"enabled\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"apply\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">#: Файлы, никогда не попадающие в архив без секретов.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_SECRET_NAMES</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".env\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_SECRET_EXT</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".session\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">#: Мусор, не попадающий в архив никогда.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_SKIP_DIRS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"__pycache__\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".git\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"node_modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_SKIP_EXT</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".pyc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".pyo\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _module_files</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(mod_dir, with_secrets):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    out </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> root, dirs, files </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.walk(mod_dir):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        dirs[:] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dirs </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _SKIP_DIRS</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fn </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(files):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.splitext(fn)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _SKIP_EXT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                continue</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> with_secrets </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    fn </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _SECRET_NAMES</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.splitext(fn)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _SECRET_EXT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            p </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(root, fn)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isfile(p) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.islink(p):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                out.append(p)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> out</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> pack_module</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(modules_dir, name, with_secrets</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Упаковать модуль в zip. Возвращает (filename, bytes).</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    with_secrets=False (дефолт): без .env/*.session — безопасно делиться.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _NAME_RX</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.match(name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"недопустимое имя\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe не упаковывается\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    mod_dir </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(modules_dir, name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isdir(mod_dir):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"модуль </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> не найден\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    manifest </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _read_manifest(mod_dir)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _check_manifest(manifest, name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    files </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _module_files(mod_dir, with_secrets)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> files:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"в модуле нет файлов для упаковки\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    buf </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> io.BytesIO()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    with</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> zipfile.ZipFile(buf, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"w\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, zipfile.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ZIP_DEFLATED</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> zf:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> p </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> files:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            rel </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.relpath(p, mod_dir).replace(os.sep, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            zf.write(p, os.path.join(name, rel))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> buf.getvalue()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    logger.info(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe: упакован </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> файлов, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> байт, secrets=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                name, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(files), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(with_secrets))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.zip\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (name, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-secrets\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> with_secrets </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), data</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> delete_module</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data_dir, modules_dir, name, must_be_disabled</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Удалить модуль: каталог + запись modstate + стейджинг.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Только выключенные (сначала modsafe.enable=false + рестарт).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    modsafe и транспорты удалять нельзя.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> modstate </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _ms</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _NAME_RX</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.match(name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"недопустимое имя\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe удалять нельзя\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    mod_dir </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(modules_dir, name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isdir(mod_dir):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"модуль </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> не найден\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    state </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _ms.load(data_dir)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    rec </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (state.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}).get(name) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> must_be_disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rec.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"enabled\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"модуль </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> включён: сначала выключите и перезапустите\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    shutil.rmtree(mod_dir)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (state.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        del</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">][name]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        _ms.save(data_dir, state)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    staging </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(data_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modstaging\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isdir(staging):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        shutil.rmtree(staging)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    logger.info(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe: удалён </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: name}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _env_path</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(modules_dir, name, env_dir</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Путь .env модуля: data/env/&#x3C;name>.env; legacy modules/&#x3C;name>/.env</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    учитывается только для флага has_env (миграция — через install.sh).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    main </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(env_dir, name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \".env\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> env_dir </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    legacy </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(modules_dir, name, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".env\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    has_legacy </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isfile(legacy)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> main </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isfile(main):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> main, has_legacy</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> has_legacy:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> legacy, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # нет нигде — писать в основной (если есть куда), иначе legacy</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (main </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> legacy), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> env_keys</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(modules_dir, name, env_dir</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Ключи секретов модуля: схема + ключи из .env (БЕЗ значений).</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Динамические секреты (auth_ref точек outbound и т.п.) нельзя перечислить</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    в схеме — их имена берутся из самого .env (dynamic: true).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _NAME_RX</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.match(name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"недопустимое имя\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    mod_dir </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(modules_dir, name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isdir(mod_dir):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"модуль </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> не найден\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    schema </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    sp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(mod_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"config.schema.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isfile(sp):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            schema </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json.load(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(sp, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">OSError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    required </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> list</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(schema.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"required\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    defaults </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(schema.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"optional\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {})</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    have </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    ep, legacy </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _env_path(modules_dir, name, env_dir)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isfile(ep):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ep, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    line </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line.strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"=\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        have.add(line.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"=\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].strip())</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> OSError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    keys </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: k, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"required\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> required, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"schema\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">             \"set\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> have, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"default\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(defaults.get(k, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> required </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> defaults </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> required]]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    seen </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {k[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> keys}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(have </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> seen):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        keys.append({</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: k, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"required\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"schema\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                     \"set\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"default\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">})</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    out </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: name, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"keys\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: keys,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">           \"has_env\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: os.path.isfile(ep)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> legacy:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"legacy_env\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> out</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> env_set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(modules_dir, name, values, env_dir</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Сохранить секреты модуля (.env 0600). Пустые значения — оставить как есть.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Ключ допустим, если он в схеме (required+optional) ЛИБО похож на имя</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    переменной окружения ([A-Z][A-Z0-9_], до 64): динамические секреты</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    (auth_ref и т.п.) в схеме не перечислить. Остальное — отказ.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _NAME_RX</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.match(name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"недопустимое имя\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    mod_dir </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(modules_dir, name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isdir(mod_dir):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"модуль </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> не найден\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    schema </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    sp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(mod_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"config.schema.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isfile(sp):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            schema </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json.load(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(sp, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">OSError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    allowed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(schema.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"required\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((schema.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"optional\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> allowed:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"у модуля </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> нет схемы секретов\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    values </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> values </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    unknown </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> values</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                     if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> allowed</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                     and</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re.fullmatch(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">[A-Z][A-Z0-9_]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">{0,63}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> unknown:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"недопустимые ключи: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \", \"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(unknown))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    ep, _legacy </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _env_path(modules_dir, name, env_dir)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    current </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    order </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isfile(ep):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ep, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    s </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line.strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"=\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        k, v </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"=\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k.strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> current:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                            order.append(k)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        current[k] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> v.strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> OSError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    changed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k, v </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> values.items():</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        v </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (v </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> v:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">  # пустое = не менять</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> current.get(k) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> v:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            current[k] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> v</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            changed.append(k)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> order:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            order.append(k)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> changed:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        parent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.dirname(ep)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> parent:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            os.makedirs(parent, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">exist_ok</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        tmp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ep </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \".tmp\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(tmp, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"w\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> order:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> current:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    f.write(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (k, current[k]))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        os.chmod(tmp, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">0o</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">600</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        os.replace(tmp, ep)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    logger.info(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe: секреты </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: изменено </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, name, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(changed))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: name, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"changed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: changed}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">#: Базовые пакеты venv — никогда не сироты.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_BASE_PKGS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> frozenset</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"pip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"setuptools\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"wheel\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">})</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _norm_pkg</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re.sub(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">[-_.]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, (name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).lower()).strip(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _req_names</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(path):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Имена пакетов из requirements.txt (без версий/маршрутов).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    out </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(path, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"r\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">errors</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"replace\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                line </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line.strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                line </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\";\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].strip()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re.match(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">^([A-Za-z0-9_.-]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, line)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    out.add(_norm_pkg(m.group(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> OSError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        pass</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> out</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> pip_orphans</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(modules_dir, transports_dir, root_dir):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Пакеты в venv, которых не требует никто (только показ, без удаления).</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    required = union requirements.txt корня + транспортов + УСТАНОВЛЕННЫХ</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    модулей (удалённого модуля уже нет в дереве — его пакеты и всплывут).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Замыкание по зависимостям зависимостей — через Requires-Dist.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Возвращает {orphans: [{name, version}], required_n, installed_n}.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> importlib </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> metadata </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _md</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ImportError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"orphans\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: [], </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"required_n\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"installed_n\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"нет importlib.metadata\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    roots </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    rp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(root_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"requirements.txt\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isfile(rp):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        roots </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _req_names(rp)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> base </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (modules_dir, transports_dir):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            entries </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(os.listdir(base))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> OSError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entry </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entries:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            fp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(base, entry, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"requirements.txt\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isfile(fp):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                roots </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _req_names(fp)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    installed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dist </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _md.distributions():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            n </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _norm_pkg(dist.metadata[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> n:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            installed.setdefault(n, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(dist.version </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # замыкание: требуемое + всё, что тянут их Requires-Dist</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    keep </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(roots) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_BASE_PKGS</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    queue </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(keep)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    while</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> queue:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        cur </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> queue.pop()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            reqs </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _md.requires(cur) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> reqs:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _norm_pkg(r.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\";\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].strip().split()[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                          .split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"[\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"(\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].rstrip(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\",\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            mm </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re.match(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">^([A-Za-z0-9_.-]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, m)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _norm_pkg(mm.group(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mm </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> keep:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                keep.add(name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                queue.append(name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    orphans </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: n, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"version\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: installed[n]}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">               for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> n </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(installed) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> n </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> keep]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"orphans\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: orphans, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"required_n\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(keep),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"installed_n\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(installed)}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> pip_uninstall_orphans</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(modules_dir, transports_dir, root_dir, packages):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Удалить пакеты-сироты тем же интерпретатором (pip uninstall -y).</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Безопасность: каждый пакет перепроверяется на сиротство ПЕРЕД удалением</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    (свежее замыкание, клиентскому списку не доверяем); базовые пакеты</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    (pip/setuptools/wheel) и пустые имена — отказ; лимит 20 за вызов.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Возвращает {removed: [...], skipped: [{name, reason}], output}.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> subprocess </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _sp</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sys </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _sys</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    wanted </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> p </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> packages </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        n </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _norm_pkg(p </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> n </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> n </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> wanted:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            wanted.append(n)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> wanted:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"пустой список пакетов\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(wanted) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 20</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"больше 20 за раз нельзя\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    fresh </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pip_orphans(modules_dir, transports_dir, root_dir)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    still </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {o[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> o </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fresh.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"orphans\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    removed, skipped, out_lines </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [], [], []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> n </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> wanted:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> n </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _BASE_PKGS</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            skipped.append({</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: n, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"reason\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"базовый пакет venv\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">})</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> n </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> still:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            skipped.append({</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: n, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"reason\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"уже требуется (не сирота)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">})</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re.fullmatch(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">[a-z0-9][a-z0-9_.-]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, n):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            skipped.append({</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: n, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"reason\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"плохое имя\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">})</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            r </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _sp.run([_sys.executable, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-m\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"pip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"uninstall\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                         \"-y\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, n],</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                        capture_output</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">timeout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">120</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            tail </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((r.stdout </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (r.stderr </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)).strip().splitlines()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out_lines </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"== </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (rc=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (n, r.returncode)]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out_lines </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [l[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">200</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> l </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tail[</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">4</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:]]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.returncode </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                removed.append(n)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                skipped.append({</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: n,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                                \"reason\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"pip rc=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.returncode})</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            skipped.append({</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: n, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"reason\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                type</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e).</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__name__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, e)})</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    logger.info(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe: pip-сироты удалены: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">, пропущены: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                removed, [s[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> skipped])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"removed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: removed, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"skipped\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: skipped,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"output\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(out_lines)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">4000</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "manifest.yaml",
          "size": 3702,
          "content": "name: modsafe\nversion: 0.4.1\ncategory: base\ndescription: \"Охрана модулей: аудит кода, хеши файлов, вкл/выкл (default-off для новых).\"\napi:\n  - modsafe.list\n  - modsafe.audit_one\n  - modsafe.set_enabled\n  - modsafe.rescan\n  - modsafe.stage\n  - modsafe.staged\n  - modsafe.install\n  - modsafe.market\n  - modsafe.delete\n  - modsafe.pack\n  - modsafe.env_keys\n  - modsafe.env_set\n  - modsafe.inspect\n  - modsafe.orphans\n  - modsafe.uninstall_orphans\napi_methods:\n  - method: list\n    args: \"{}\"\n    returns: \"{modules: [...], note}\"\n    desc: \"Все известные модули/транспорты со статусом\"\n  - method: audit_one\n    args: \"{name: str}\"\n    returns: \"{summary, findings}\"\n    desc: \"Находки сканера по одному модулю\"\n  - method: set_enabled\n    args: \"{name: str, enabled: bool}\"\n    returns: \"{ok, apply: restart}\"\n    desc: \"Вкл/выкл модуля (применится после рестарта; себя выключить нельзя)\"\n  - method: rescan\n    args: \"{name?: str}\"\n    returns: \"{rescanned: [...]}\"\n    desc: \"Обновить эталон хешей (текущие файлы = доверенные)\"\n  - method: stage\n    args: \"{filename, content_type, body}\"\n    returns: \"{name, version, summary, findings}\"\n    desc: \"Принять архив в стейджинг + аудит\"\n  - method: staged\n    args: \"{}\"\n    returns: \"{staged: [...]}\"\n    desc: \"Список стейджинга\"\n  - method: install\n    args: \"{name: str}\"\n    returns: \"{ok, enabled: false}\"\n    desc: \"Установить из стейджинга (выключенным)\"\n  - method: market\n    args: \"{}\"\n    returns: \"{stub: true}\"\n    desc: \"Маркет модулей (заглушка)\"\n  - method: delete\n    args: \"{name: str}\"\n    returns: \"{ok}\"\n    desc: \"Удалить модуль (только выключенный)\"\n  - method: pack\n    args: \"{name: str, with_secrets: bool}\"\n    returns: \"{filename, size, b64}\"\n    desc: \"Упаковать модуль в zip (b64)\"\n  - method: env_keys\n    args: \"{name: str}\"\n    returns: \"{keys: [{key, required, set, default}]}\"\n    desc: \"Ключи секретов без значений\"\n  - method: env_set\n    args: \"{name: str, values: {...}}\"\n    returns: \"{changed: [...]}\"\n    desc: \"Сохранить секреты (.env 0600)\"\n  - method: inspect\n    args: \"{name: str}\"\n    returns: \"{manifest, api_live, rights, ui_web, tg_menu, ws, readme, files, health, ...}\"\n    desc: \"Полный паспорт модуля/транспорта\"\n  - method: orphans\n    args: \"{}\"\n    returns: \"{orphans: [{name, version}], required_n, installed_n}\"\n    desc: \"pip-пакеты, которых не требует никто (только показ)\"\n  - method: uninstall_orphans\n    args: \"{packages: [...]}\"\n    returns: \"{removed, skipped, output}\"\n    desc: \"Удалить сирот (с перепроверкой, лимит 20)\"\nevents_emitted: []\nevents_subscribed: []\naudit_allow:\n  - RM\n  - JS_INNERHTML\n  - JS_ATOB\nrights:\n  - name: modules_manage\n    description: \"Управление модулями: вкл/выкл, аудит, эталон хешей\"\nweb_routes:\n  - GET /api/modsafe/modules\n  - POST /api/modsafe/enable\n  - POST /api/modsafe/rescan\n  - POST /api/modsafe/upload\n  - GET /api/modsafe/staged\n  - POST /api/modsafe/install\n  - GET /api/modsafe/market\n  - POST /api/modsafe/delete\n  - GET /api/modsafe/pack\n  - GET /api/modsafe/env\n  - POST /api/modsafe/env\n  - GET /api/modsafe/inspect\n  - GET /api/modsafe/orphans\nconfig_schema: config.schema.json\nenv_example: .env.example\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modsafe</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">version</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0.4.1</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">category</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">base</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Охрана модулей: аудит кода, хеши файлов, вкл/выкл (default-off для новых).\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modsafe.list</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modsafe.audit_one</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modsafe.set_enabled</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modsafe.rescan</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modsafe.stage</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modsafe.staged</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modsafe.install</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modsafe.market</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modsafe.delete</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modsafe.pack</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modsafe.env_keys</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modsafe.env_set</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modsafe.inspect</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modsafe.orphans</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modsafe.uninstall_orphans</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">api_methods</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">list</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{modules: [...], note}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Все известные модули/транспорты со статусом\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">audit_one</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{name: str}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{summary, findings}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Находки сканера по одному модулю\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">set_enabled</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{name: str, enabled: bool}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{ok, apply: restart}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Вкл/выкл модуля (применится после рестарта; себя выключить нельзя)\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">rescan</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{name?: str}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{rescanned: [...]}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Обновить эталон хешей (текущие файлы = доверенные)\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">stage</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{filename, content_type, body}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{name, version, summary, findings}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Принять архив в стейджинг + аудит\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">staged</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{staged: [...]}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Список стейджинга\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">install</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{name: str}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{ok, enabled: false}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Установить из стейджинга (выключенным)\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">market</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{stub: true}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Маркет модулей (заглушка)\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">delete</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{name: str}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{ok}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Удалить модуль (только выключенный)\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">pack</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{name: str, with_secrets: bool}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{filename, size, b64}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Упаковать модуль в zip (b64)\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">env_keys</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{name: str}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{keys: [{key, required, set, default}]}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ключи секретов без значений\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">env_set</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{name: str, values: {...}}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{changed: [...]}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Сохранить секреты (.env 0600)\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">inspect</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{name: str}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{manifest, api_live, rights, ui_web, tg_menu, ws, readme, files, health, ...}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Полный паспорт модуля/транспорта\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">orphans</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{orphans: [{name, version}], required_n, installed_n}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"pip-пакеты, которых не требует никто (только показ)\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">uninstall_orphans</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{packages: [...]}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{removed, skipped, output}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Удалить сирот (с перепроверкой, лимит 20)\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">events_emitted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: []</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">events_subscribed</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: []</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">audit_allow</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">RM</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">JS_INNERHTML</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">JS_ATOB</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">rights</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">modules_manage</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Управление модулями: вкл/выкл, аудит, эталон хешей\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">web_routes</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/modsafe/modules</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">POST /api/modsafe/enable</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">POST /api/modsafe/rescan</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">POST /api/modsafe/upload</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/modsafe/staged</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">POST /api/modsafe/install</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/modsafe/market</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">POST /api/modsafe/delete</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/modsafe/pack</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/modsafe/env</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">POST /api/modsafe/env</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/modsafe/inspect</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/modsafe/orphans</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">config_schema</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">config.schema.json</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">env_example</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.env.example</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "module.py",
          "size": 17504,
          "content": "\"\"\"modsafe.Module — охрана модулей: список, аудит, вкл/выкл, хеши.\n\nЧитает ctx.module_audit (считает loader при старте: сканер + сверка хешей\nДО импорта кода) и core.modstate (вкл/выкл, эталон хешей). Изменения\nвкл/выкл и эталона применяются после рестарта бота (loader-time).\n\"\"\"\nimport logging\nimport os\n\nfrom core.base_module import BaseModule\n\nlogger = logging.getLogger(__name__)\n\n\nclass Module(BaseModule):\n    def setup(self, ctx) -> None:\n        self.ctx = ctx\n        self.config = ctx.module_config(\"modsafe\")\n        from core import modstate as _ms\n        self._ms = _ms\n        ctx.api.register(\"modsafe\", \"list\", self.api_list)\n        ctx.api.register(\"modsafe\", \"audit_one\", self.api_audit_one)\n        ctx.api.register(\"modsafe\", \"set_enabled\", self.api_set_enabled)\n        ctx.api.register(\"modsafe\", \"rescan\", self.api_rescan)\n        ctx.api.register(\"modsafe\", \"stage\", self.api_stage)\n        ctx.api.register(\"modsafe\", \"staged\", self.api_staged)\n        ctx.api.register(\"modsafe\", \"install\", self.api_install)\n        ctx.api.register(\"modsafe\", \"market\", self.api_market)\n        ctx.api.register(\"modsafe\", \"delete\", self.api_delete)\n        ctx.api.register(\"modsafe\", \"pack\", self.api_pack)\n        ctx.api.register(\"modsafe\", \"env_keys\", self.api_env_keys)\n        ctx.api.register(\"modsafe\", \"env_set\", self.api_env_set)\n        ctx.api.register(\"modsafe\", \"inspect\", self.api_inspect)\n        ctx.api.register(\"modsafe\", \"orphans\", self.api_orphans)\n        ctx.api.register(\"modsafe\", \"uninstall_orphans\",\n                         self.api_uninstall_orphans)\n\n    # --- helpers ---\n\n    def _audit(self):\n        return getattr(self.ctx, \"module_audit\", {}) or {}\n\n    def _mod_dir(self, name):\n        base = os.path.join(getattr(self.ctx, \"modules_dir\", \"\"), name or \"\")\n        if os.path.isfile(os.path.join(base, \"manifest.yaml\")) or \\\n                os.path.isfile(os.path.join(base, \"manifest.json\")):\n            return base\n        return \"\"\n\n    def _any_dir(self, name):\n        \"\"\"Каталог модуля или транспорта (для inspect).\"\"\"\n        d = self._mod_dir(name)\n        if d:\n            return d, False\n        tbase = os.path.join(\n            getattr(self.ctx, \"transports_dir\", \"\"), name or \"\")\n        if os.path.isfile(os.path.join(tbase, \"manifest.yaml\")):\n            return tbase, True\n        return \"\", False\n\n    def api_inspect(self, name):\n        \"\"\"Полный паспорт модуля/транспорта: манифест, API, права, вклады\n        (роуты, сервисы, панели, nav, TG-кнопки, WS), README, файлы, health,\n        аудит. Право modules_manage. Всё best-effort: отсутствующее — пусто.\n        \"\"\"\n        import importlib as _il\n        name = (name or \"\").strip()\n        if not name or any(c not in\n                           \"abcdefghijklmnopqrstuvwxyz0123456789-_\"\n                           for c in name.lower()):\n            raise ValueError(\"плохое имя: %s\" % name)\n        out = {\"name\": name}\n        mod_dir, is_transport = self._any_dir(name)\n        if not mod_dir:\n            raise ValueError(\"модуль %s неизвестен\" % name)\n        out[\"transport\"] = is_transport\n\n        manifest = {}\n        for fn in (\"manifest.yaml\", \"manifest.json\"):\n            fp = os.path.join(mod_dir, fn)\n            if os.path.isfile(fp):\n                try:\n                    if fn.endswith(\".yaml\"):\n                        import yaml as _yaml\n                        manifest = _yaml.safe_load(\n                            open(fp, encoding=\"utf-8\")) or {}\n                    else:\n                        import json as _json\n                        manifest = _json.load(\n                            open(fp, encoding=\"utf-8\"))\n                except Exception:  # noqa: BLE001\n                    manifest = {}\n                break\n        out[\"manifest\"] = manifest\n\n        loaded = None\n        try:\n            for m in self.ctx.loaded_modules or []:\n                if m.name == name:\n                    loaded = m\n                    break\n        except Exception:  # noqa: BLE001\n            loaded = None\n        out[\"loaded\"] = loaded is not None\n        if loaded is not None:\n            try:\n                out[\"health\"] = loaded.health()\n            except Exception as e:  # noqa: BLE001\n                out[\"health\"] = {\"ok\": False,\n                                 \"error\": \"%s: %s\" % (type(e).__name__, e)}\n\n        try:\n            described = (self.ctx.api.describe() or {}).get(name) or []\n        except Exception:  # noqa: BLE001\n            described = []\n        out[\"api_live\"] = described\n\n        try:\n            rights = [r for r in self.ctx.rights.list_rights()\n                      if (r.get(\"module\") or \"\") == name]\n        except Exception:  # noqa: BLE001\n            rights = []\n        out[\"rights\"] = rights\n        try:\n            out[\"entry_defaults\"] = self.ctx.rights.entry_defaults(name)\n        except Exception:  # noqa: BLE001\n            out[\"entry_defaults\"] = {}\n\n        ui = {}\n        try:\n            mod = _il.import_module(\"botmod_%s.ui_web\" % name)\n            for attr in (\"ROUTES\", \"SERVICE\", \"PANELS\", \"NAV\"):\n                raw = getattr(mod, attr, None)\n                if raw is None:\n                    continue\n                items = raw if isinstance(raw, list) else [raw]\n                ui[attr.lower()] = [_safe(o) for o in items if o]\n        except Exception:  # noqa: BLE001\n            pass\n        out[\"ui_web\"] = ui\n        try:\n            tg = _il.import_module(\"botmod_%s.ui_tg\" % name)\n            menu = getattr(tg, \"TG_MENU\", None)\n            items = menu if isinstance(menu, list) else [menu]\n            out[\"tg_menu\"] = [\n                {\"id\": i.get(\"id\"), \"title\": i.get(\"title\"),\n                 \"right\": i.get(\"right\")}\n                for i in items if isinstance(i, dict)]\n        except Exception:  # noqa: BLE001\n            out[\"tg_menu\"] = []\n        try:\n            ws = _il.import_module(\"botmod_%s.ui_ws\" % name)\n            out[\"ws\"] = {\"path\": getattr(ws, \"WS_PATH\", \"\") or \"\",\n                         \"right\": getattr(ws, \"RIGHT\", \"\") or \"\"}\n        except Exception:  # noqa: BLE001\n            out[\"ws\"] = {}\n\n        readme = \"\"\n        for fn in (\"README.md\", \"README\"):\n            fp = os.path.join(mod_dir, fn)\n            if os.path.isfile(fp):\n                try:\n                    with open(fp, \"r\", encoding=\"utf-8\",\n                              errors=\"replace\") as f:\n                        readme = f.read(30000)\n                except Exception:  # noqa: BLE001\n                    readme = \"\"\n                break\n        out[\"readme\"] = readme\n\n        a = self._audit().get(name) or {}\n        out[\"enabled\"] = True if name == \"modsafe\" else bool(a.get(\"enabled\"))\n        out[\"audit_summary\"] = a.get(\"summary\") or {}\n        out[\"hashes\"] = a.get(\"hashes\") or {}\n        out[\"files\"] = []\n        try:\n            from core import modstate as _ms\n            rec = (_ms.load(\n                getattr(self.ctx, \"data_dir\", \".\")) or {}).get(\n                    \"modules\", {}).get(name) or {}\n            out[\"files\"] = sorted((rec.get(\"hashes\") or {}).keys())\n        except Exception:  # noqa: BLE001\n            pass\n        return out\n\n\n\n    # --- api ---\n\n    def api_list(self):\n        \"\"\"Все известные модули/транспорты со статусом (право modules_manage).\"\"\"\n        out = []\n        for name in sorted(self._audit()):\n            a = self._audit()[name]\n            out.append({\n                \"name\": name,\n                \"version\": a.get(\"version\") or \"\",\n                \"description\": a.get(\"description\") or \"\",\n                \"category\": a.get(\"category\") or \"extra\",\n                # modsafe грузится всегда (инфраструктура) — показываем факт\n                \"enabled\": True if name == \"modsafe\" else bool(a.get(\"enabled\")),\n                \"transport\": bool(a.get(\"transport\")),\n                \"summary\": a.get(\"summary\") or {},\n                \"hashes\": a.get(\"hashes\") or {},\n            })\n        return {\"modules\": out,\n                \"note\": \"Вкл/выкл применяется после рестарта бота.\"}\n\n    def api_audit_one(self, name):\n        \"\"\"Находки одного модуля (право modules_manage).\"\"\"\n        a = self._audit().get(name or \"\")\n        if a is None:\n            raise ValueError(\"Модуль %s неизвестен\" % name)\n        return {\"name\": name, \"summary\": a.get(\"summary\") or {},\n                \"findings\": a.get(\"findings\") or []}\n\n    def _audit_rec(self, action, target, actor=\"\", detail=\"\", ok=True):\n        try:\n            self.ctx.api.call('audit.record', actor or '?', action,\n                              target, detail, ok)\n        except Exception:  # noqa: BLE001\n            pass\n\n    def api_set_enabled(self, name, enabled, actor=''):\n        \"\"\"Вкл/выкл модуля (право modules_manage). Себя выключить нельзя.\"\"\"\n        if (name or \"\") == \"modsafe\" and not enabled:\n            raise ValueError(\"modsafe нельзя выключить (иначе нечем включить)\")\n        ok, err = self._ms.set_enabled(\n            getattr(self.ctx, \"data_dir\", \".\"), name, bool(enabled))\n        if not ok:\n            raise ValueError(err)\n        # освежаем кэш аудита для UI\n        try:\n            self._audit()[name][\"enabled\"] = bool(enabled)\n        except Exception:  # noqa: BLE001\n            pass\n        logger.info(\"modsafe: модуль %s -> %s\",\n                    name, \"enabled\" if enabled else \"disabled\")\n        self._audit_rec('modules.enable' if enabled else 'modules.disable',\n                        name, actor or '?')\n        return {\"ok\": True, \"name\": name, \"enabled\": bool(enabled),\n                \"apply\": \"restart\"}\n\n    def api_rescan(self, name=None):\n        \"\"\"Пересчитать эталон хешей (и свежий скан) модуля или всех.\n\n        Право modules_manage. Явное действие — фиксирует текущие файлы\n        как доверенные (после проверки находок).\n        \"\"\"\n        from core import audit as _audit\n        data_dir = getattr(self.ctx, \"data_dir\", \".\")\n        names = [name] if name else sorted(self._audit())\n        done = []\n        for n in names:\n            d = self._mod_dir(n)\n            if not d and not self._audit().get(n, {}).get(\"transport\"):\n                continue\n            if not d:\n                # транспорт: ищем каталог в transports_dir\n                tbase = os.path.join(\n                    getattr(self.ctx, \"transports_dir\", \"\"), n or \"\")\n                if os.path.isdir(tbase):\n                    d = tbase\n                else:\n                    continue\n            hashes = _audit.hash_module(d)\n            self._ms.update_hashes(data_dir, n, hashes)\n            try:\n                self._audit()[n][\"hashes\"] = {\n                    \"ok\": True, \"changed\": [], \"new\": [], \"missing\": [],\n                    \"baseline_at\": \"now\"}\n            except Exception:  # noqa: BLE001\n                pass\n            done.append(n)\n        logger.info(\"modsafe: эталон хешей обновлён: %s\", \", \".join(done))\n        return {\"ok\": True, \"rescanned\": done}\n\n    def api_stage(self, filename, content_type, body):\n        \"\"\"Принять архив модуля в стейджинг + аудит (право modules_manage).\"\"\"\n        from . import installer as _inst\n        try:\n            return _inst.stage_upload(\n                getattr(self.ctx, \"data_dir\", \".\"),\n                getattr(self.ctx, \"modules_dir\", \"modules\"),\n                self.config, filename, body)\n        except ValueError as e:\n            raise ValueError(str(e))\n\n    def api_staged(self):\n        \"\"\"Список стейджинга (право modules_manage).\"\"\"\n        from . import installer as _inst\n        return {\"staged\": _inst.list_staged(getattr(self.ctx, \"data_dir\", \".\"))}\n\n    def api_install(self, name, actor=''):\n        \"\"\"Установить из стейджинга (выключенным; право modules_manage).\"\"\"\n        from . import installer as _inst\n        try:\n            out = _inst.install_staged(\n                getattr(self.ctx, \"data_dir\", \".\"),\n                getattr(self.ctx, \"modules_dir\", \"modules\"), name)\n            self._audit_rec('modules.install', name, actor)\n            return out\n        except ValueError as e:\n            raise ValueError(str(e))\n\n    def api_market(self):\n        \"\"\"Маркет модулей — ЗАГЛУШКА до запуска страницы.\"\"\"\n        url = \"\"\n        try:\n            url = (self.config.get(\"MODSAFE_MARKET_URL\", \"\") or \"\").strip()\n        except Exception:  # noqa: BLE001\n            pass\n        return {\"ok\": True, \"stub\": True, \"url\": url,\n                \"modules\": [],\n                \"note\": \"Маркет ещё не запущен. Загрузка — архивом выше.\"}\n\n    def api_delete(self, name, actor=''):\n        \"\"\"Удалить модуль (только выключенный; право modules_manage).\"\"\"\n        from core import modstate as _ms\n        if (name or \"\") in _ms.CORE_MODULES:\n            raise ValueError(\"модуль %s системный (core), удалить нельзя\" % name)\n        from . import installer as _inst\n        try:\n            out = _inst.delete_module(\n                getattr(self.ctx, \"data_dir\", \".\"),\n                getattr(self.ctx, \"modules_dir\", \"modules\"), name)\n            self._audit_rec('modules.delete', name, actor)\n            return out\n        except ValueError as e:\n            raise ValueError(str(e))\n\n    def api_pack(self, name, with_secrets=False):\n        \"\"\"Упаковать модуль в zip (b64; право modules_manage).\"\"\"\n        import base64 as _b64\n        from . import installer as _inst\n        try:\n            filename, data = _inst.pack_module(\n                getattr(self.ctx, \"modules_dir\", \"modules\"),\n                name, bool(with_secrets))\n        except ValueError as e:\n            raise ValueError(str(e))\n        return {\"ok\": True, \"filename\": filename, \"size\": len(data),\n                \"with_secrets\": bool(with_secrets),\n                \"b64\": _b64.b64encode(data).decode(\"ascii\")}\n\n    def _env_dir(self):\n        try:\n            return os.path.join(self.ctx.data_dir, \"env\")\n        except Exception:  # noqa: BLE001\n            return None\n\n    def api_env_keys(self, name):\n        \"\"\"Ключи секретов модуля без значений (право modules_manage).\"\"\"\n        from . import installer as _inst\n        try:\n            return _inst.env_keys(\n                getattr(self.ctx, \"modules_dir\", \"modules\"), name,\n                self._env_dir())\n        except ValueError as e:\n            raise ValueError(str(e))\n\n    def api_env_set(self, name, values, actor=''):\n        \"\"\"Сохранить секреты модуля (право modules_manage).\"\"\"\n        from . import installer as _inst\n        try:\n            out = _inst.env_set(\n                getattr(self.ctx, \"modules_dir\", \"modules\"),\n                name, values or {}, self._env_dir())\n            self._audit_rec('secrets.set', name, actor,\n                            'keys=%s' % ','.join(out.get('changed') or []))\n            return out\n        except ValueError as e:\n            raise ValueError(str(e))\n\n    def api_orphans(self):\n        \"\"\"pip-пакеты, которых не требует никто (только показ; modules_manage).\"\"\"\n        from . import installer as _inst\n        import os as _os\n        mods = getattr(self.ctx, \"modules_dir\", \"modules\")\n        root = _os.path.dirname(_os.path.abspath(mods))\n        return _inst.pip_orphans(mods, _os.path.join(root, \"transports\"),\n                                 root)\n\n    def api_uninstall_orphans(self, packages):\n        \"\"\"Удалить пакеты-сироты (перепроверка + modules_manage).\"\"\"\n        from . import installer as _inst\n        import os as _os\n        mods = getattr(self.ctx, \"modules_dir\", \"modules\")\n        root = _os.path.dirname(_os.path.abspath(mods))\n        try:\n            return _inst.pip_uninstall_orphans(\n                mods, _os.path.join(root, \"transports\"), root,\n                packages or [])\n        except ValueError as e:\n            raise ValueError(str(e))\n\n    def health(self) -> dict:\n        n = len(self._audit())\n        crit = sum((a.get(\"summary\") or {}).get(\"critical\", 0)\n                   for a in self._audit().values())\n        return {\"ok\": True, \"module\": self.name,\n                \"known\": n, \"critical\": crit}\n\ndef _safe(o):\n    \"\"\"JSON-безопасная копия вклада (tuples ROUTES -> lists).\"\"\"\n    import json as _json\n    try:\n        return _json.loads(_json.dumps(o, ensure_ascii=False, default=str))\n    except Exception:  # noqa: BLE001\n        return {\"repr\": str(o)[:500]}\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"modsafe.Module — охрана модулей: список, аудит, вкл/выкл, хеши.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Читает ctx.module_audit (считает loader при старте: сканер + сверка хешей</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">ДО импорта кода) и core.modstate (вкл/выкл, эталон хешей). Изменения</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">вкл/выкл и эталона применяются после рестарта бота (loader-time).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.base_module </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> BaseModule</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">logger </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging.getLogger(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__name__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">class</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Module</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">BaseModule</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> setup</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, ctx) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.module_config(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> modstate </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _ms</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._ms </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _ms</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"list\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.api_list)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"audit_one\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.api_audit_one)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"set_enabled\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.api_set_enabled)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"rescan\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.api_rescan)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"stage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.api_stage)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"staged\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.api_staged)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"install\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.api_install)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"market\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.api_market)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"delete\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.api_delete)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"pack\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.api_pack)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"env_keys\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.api_env_keys)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"env_set\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.api_env_set)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"inspect\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.api_inspect)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"orphans\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.api_orphans)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"uninstall_orphans\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                         self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.api_uninstall_orphans)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # --- helpers ---</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _audit</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"module_audit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {}) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _mod_dir</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, name):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        base </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isfile(os.path.join(base, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manifest.yaml\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                os.path.isfile(os.path.join(base, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manifest.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> base</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _any_dir</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, name):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Каталог модуля или транспорта (для inspect).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._mod_dir(name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        tbase </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"transports_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isfile(os.path.join(tbase, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manifest.yaml\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tbase, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api_inspect</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, name):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Полный паспорт модуля/транспорта: манифест, API, права, вклады</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        (роуты, сервисы, панели, nav, TG-кнопки, WS), README, файлы, health,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        аудит. Право modules_manage. Всё best-effort: отсутствующее — пусто.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> importlib </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _il</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> any</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(c </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                           \"abcdefghijklmnopqrstuvwxyz0123456789-_\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                           for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> c </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name.lower()):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"плохое имя: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: name}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mod_dir, is_transport </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._any_dir(name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mod_dir:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"модуль </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> неизвестен\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"transport\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> is_transport</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        manifest </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fn </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manifest.yaml\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manifest.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            fp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(mod_dir, fn)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isfile(fp):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fn.endswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".yaml\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> yaml </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _yaml</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        manifest </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _yaml.safe_load(</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                            open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(fp, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _json</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        manifest </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _json.load(</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                            open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(fp, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    manifest </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                break</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manifest\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> manifest</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        loaded </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx.loaded_modules </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m.name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    loaded </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    break</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            loaded </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"loaded\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> loaded </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> loaded </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"health\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> loaded.health()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"health\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                                 \"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">type</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e).</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__name__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, e)}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            described </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx.api.describe() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}).get(name) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            described </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"api_live\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> described</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            rights </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [r </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx.rights.list_rights()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                      if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (r.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"module\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            rights </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"rights\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rights</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"entry_defaults\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx.rights.entry_defaults(name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"entry_defaults\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ui </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            mod </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _il.import_module(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"botmod_</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.ui_web\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> attr </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ROUTES\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SERVICE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"PANELS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"NAV\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                raw </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(mod, attr, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> raw </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                items </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> raw </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(raw, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">list</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [raw]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                ui[attr.lower()] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [_safe(o) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> o </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> items </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> o]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ui_web\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ui</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            tg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _il.import_module(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"botmod_</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.ui_tg\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            menu </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(tg, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"TG_MENU\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            items </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> menu </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(menu, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">list</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [menu]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"tg_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: i.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: i.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                 \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: i.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> i </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> items </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(i, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"tg_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ws </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _il.import_module(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"botmod_</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.ui_ws\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ws\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"path\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ws, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"WS_PATH\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                         \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ws, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"RIGHT\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ws\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        readme </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fn </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"README.md\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"README\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            fp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(mod_dir, fn)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isfile(fp):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(fp, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"r\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                              errors</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"replace\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        readme </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f.read(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">30000</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    readme </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                break</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"readme\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> readme</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        a </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._audit().get(name) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"enabled\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"modsafe\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(a.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"enabled\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"audit_summary\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> a.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"summary\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hashes\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> a.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hashes\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"files\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> modstate </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _ms</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            rec </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (_ms.load(</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"data_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}).get(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {}).get(name) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"files\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((rec.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hashes\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}).keys())</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> out</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # --- api ---</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api_list</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Все известные модули/транспорты со статусом (право modules_manage).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._audit()):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            a </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._audit()[name]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out.append({</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: name,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"version\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: a.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"version\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"description\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: a.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"description\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"category\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: a.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"category\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"extra\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                # modsafe грузится всегда (инфраструктура) — показываем факт</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"enabled\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"modsafe\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(a.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"enabled\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"transport\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(a.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"transport\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"summary\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: a.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"summary\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {},</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"hashes\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: a.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hashes\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            })</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: out,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"note\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Вкл/выкл применяется после рестарта бота.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api_audit_one</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, name):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Находки одного модуля (право modules_manage).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        a </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._audit().get(name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> a </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Модуль </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> неизвестен\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: name, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"summary\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: a.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"summary\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {},</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"findings\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: a.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"findings\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _audit_rec</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, action, target, actor</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, detail</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, ok</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'audit.record'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, actor </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '?'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, action,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                              target, detail, ok)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api_set_enabled</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, name, enabled, actor</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">''</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Вкл/выкл модуля (право modules_manage). Себя выключить нельзя.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"modsafe\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> and</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> enabled:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe нельзя выключить (иначе нечем включить)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ok, err </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._ms.set_enabled(</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"data_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), name, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(enabled))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ok:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(err)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # освежаем кэш аудита для UI</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._audit()[name][</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"enabled\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(enabled)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        logger.info(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe: модуль </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    name, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"enabled\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> enabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"disabled\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._audit_rec(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'modules.enable'</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> enabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> 'modules.disable'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        name, actor </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '?'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: name, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"enabled\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(enabled),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"apply\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api_rescan</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, name</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Пересчитать эталон хешей (и свежий скан) модуля или всех.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Право modules_manage. Явное действие — фиксирует текущие файлы</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        как доверенные (после проверки находок).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> audit </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _audit</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        data_dir </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"data_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        names </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [name] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._audit())</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        done </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> n </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> names:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._mod_dir(n)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._audit().get(n, {}).get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"transport\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                continue</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                # транспорт: ищем каталог в transports_dir</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                tbase </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"transports_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), n </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isdir(tbase):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tbase</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            hashes </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _audit.hash_module(d)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._ms.update_hashes(data_dir, n, hashes)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._audit()[n][</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hashes\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"changed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: [], </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"new\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: [], </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"missing\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: [],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"baseline_at\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"now\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            done.append(n)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        logger.info(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe: эталон хешей обновлён: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\", \"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(done))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"rescanned\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: done}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api_stage</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, filename, content_type, body):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Принять архив модуля в стейджинг + аудит (право modules_manage).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> installer </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst.stage_upload(</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"data_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config, filename, body)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api_staged</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Список стейджинга (право modules_manage).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> installer </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"staged\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _inst.list_staged(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"data_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api_install</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, name, actor</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">''</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Установить из стейджинга (выключенным; право modules_manage).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> installer </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst.install_staged(</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"data_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), name)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._audit_rec(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'modules.install'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, name, actor)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> out</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api_market</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Маркет модулей — ЗАГЛУШКА до запуска страницы.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        url </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            url </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MODSAFE_MARKET_URL\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"stub\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"url\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: url,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: [],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"note\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Маркет ещё не запущен. Загрузка — архивом выше.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api_delete</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, name, actor</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">''</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Удалить модуль (только выключенный; право modules_manage).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> modstate </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _ms</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _ms.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">CORE_MODULES</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"модуль </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> системный (core), удалить нельзя\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> installer </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst.delete_module(</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"data_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), name)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._audit_rec(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'modules.delete'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, name, actor)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> out</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api_pack</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, name, with_secrets</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Упаковать модуль в zip (b64; право modules_manage).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> base64 </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _b64</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> installer </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            filename, data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst.pack_module(</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                name, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(with_secrets))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"filename\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: filename, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"size\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"with_secrets\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(with_secrets),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"b64\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _b64.b64encode(data).decode(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ascii\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _env_dir</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx.data_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"env\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api_env_keys</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, name):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Ключи секретов модуля без значений (право modules_manage).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> installer </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst.env_keys(</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), name,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._env_dir())</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api_env_set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, name, values, actor</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">''</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Сохранить секреты модуля (право modules_manage).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> installer </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst.env_set(</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                name, values </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._env_dir())</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._audit_rec(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'secrets.set'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, name, actor,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                            'keys=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> ','</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(out.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'changed'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> out</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api_orphans</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"pip-пакеты, которых не требует никто (только показ; modules_manage).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> installer </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _os</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mods </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        root </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _os.path.dirname(_os.path.abspath(mods))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst.pip_orphans(mods, _os.path.join(root, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"transports\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                                 root)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api_uninstall_orphans</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, packages):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Удалить пакеты-сироты (перепроверка + modules_manage).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> installer </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _os</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mods </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_dir\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        root </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _os.path.dirname(_os.path.abspath(mods))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _inst.pip_uninstall_orphans(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                mods, _os.path.join(root, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"transports\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), root,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                packages </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> health</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        n </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._audit())</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        crit </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sum</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((a.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"summary\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}).get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"critical\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                   for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> a </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._audit().values())</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"module\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.name,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"known\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: n, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"critical\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: crit}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _safe</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(o):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"JSON-безопасная копия вклада (tuples ROUTES -> lists).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _json</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _json.loads(_json.dumps(o, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">ensure_ascii</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">default</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"repr\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(o)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "ui_web.py",
          "size": 6771,
          "content": "\"\"\"modsafe.ui_web — API кабинета + карточка «Модули и безопасность».\"\"\"\nROUTES = [\n    (\"GET\", \"/api/modsafe/modules\", \"modules\", {\"right\": \"modules_manage\",\n     \"desc\": \"Модули: статус, аудит, хеши\"}),\n    (\"POST\", \"/api/modsafe/enable\", \"enable\", {\"right\": \"modules_manage\",\n     \"desc\": \"Вкл/выкл модуля {name, enabled} (применится после рестарта)\"}),\n    (\"POST\", \"/api/modsafe/rescan\", \"rescan\", {\"right\": \"modules_manage\",\n     \"desc\": \"Обновить эталон хешей {name?}\"}),\n    (\"POST\", \"/api/modsafe/upload\", \"upload\", {\"right\": \"modules_manage\",\n     \"desc\": \"Загрузить архив модуля (multipart file=*.zip) в стейджинг\"}),\n    (\"GET\", \"/api/modsafe/staged\", \"staged\", {\"right\": \"modules_manage\",\n     \"desc\": \"Стейджинг загруженных модулей\"}),\n    (\"POST\", \"/api/modsafe/install\", \"install\", {\"right\": \"modules_manage\",\n     \"desc\": \"Установить модуль из стейджинга {name} (выключенным)\"}),\n    (\"GET\", \"/api/modsafe/market\", \"market\", {\"right\": \"modules_manage\",\n     \"desc\": \"Маркет модулей (заглушка до запуска страницы)\"}),\n    (\"POST\", \"/api/modsafe/delete\", \"delete\", {\"right\": \"modules_manage\",\n     \"desc\": \"Удалить модуль {name} (только выключенный)\"}),\n    (\"GET\", \"/api/modsafe/pack\", \"pack\", {\"right\": \"modules_manage\",\n     \"desc\": \"Скачать модуль zip (?name, ?secrets=1)\"}),\n    (\"GET\", \"/api/modsafe/env\", \"env_keys\", {\"right\": \"modules_manage\",\n     \"desc\": \"Ключи секретов модуля (?name, без значений)\"}),\n    (\"POST\", \"/api/modsafe/env\", \"env_set\", {\"right\": \"modules_manage\",\n     \"desc\": \"Сохранить секреты модуля {name, values}\"}),\n    (\"GET\", \"/api/modsafe/inspect\", \"inspect\", {\"right\": \"modules_manage\",\n     \"desc\": \"Паспорт модуля (?name): манифест, API, права, вклады, README\"}),\n    (\"GET\", \"/api/modsafe/orphans\", \"orphans\", {\"right\": \"modules_manage\",\n     \"desc\": \"pip-сироты: пакеты, которых не требует никто (только показ)\"}),\n    (\"POST\", \"/api/modsafe/orphans\", \"uninstall_orphans\", {\"right\": \"modules_manage\",\n     \"desc\": \"Удалить пакеты-сироты {packages: [...]} (с перепроверкой)\"}),\n]\n\nSERVICE = {\"key\": \"modsafe\", \"icon\": \"🧩\", \"title\": \"Модули и безопасность\",\n           \"right\": \"modules_manage\", \"phase\": 1,\n           \"desc\": \"Вкл/выкл модулей, аудит кода, сверка хешей\"}\n\nPANELS = []\n\ndef _actor(req):\n    sess = req.get('session') or {}\n    return 'web:%s' % sess.get('uid') if sess.get('uid') else '?'\n\n\n\ndef handle_api(ctx, config, method, req):\n    from core.errors import UserError\n\n    if method == \"modules\":\n        query = req.get(\"query\") or {}\n        if query.get(\"findings\"):\n            try:\n                return ctx.api.call(\"modsafe.audit_one\", query.get(\"findings\"))\n            except ValueError as e:\n                return 400, {\"ok\": False, \"error\": str(e)}\n        return ctx.api.call(\"modsafe.list\")\n    if method == \"enable\":\n        body = req.get(\"body\") or {}\n        try:\n            return ctx.api.call(\"modsafe.set_enabled\",\n                                body.get(\"name\"), body.get(\"enabled\"),\n                                _actor(req))\n        except ValueError as e:\n            return 400, {\"ok\": False, \"error\": str(e)}\n    if method == \"rescan\":\n        body = req.get(\"body\") or {}\n        try:\n            return ctx.api.call(\"modsafe.rescan\", body.get(\"name\"))\n        except ValueError as e:\n            return 400, {\"ok\": False, \"error\": str(e)}\n    if method == \"upload\":\n        files = (req.get(\"files\") or {}).get(\"file\") or []\n        if not files:\n            return 400, {\"ok\": False, \"error\": \"Файл не передан (multipart file=*.zip)\"}\n        fi = files[0]\n        try:\n            return ctx.api.call(\"modsafe.stage\", fi.get(\"filename\") or \"upload.zip\",\n                                fi.get(\"content_type\") or \"\", fi.get(\"body\") or b\"\")\n        except ValueError as e:\n            return 400, {\"ok\": False, \"error\": str(e)}\n    if method == \"staged\":\n        return ctx.api.call(\"modsafe.staged\")\n    if method == \"install\":\n        body = req.get(\"body\") or {}\n        try:\n            return ctx.api.call(\"modsafe.install\", body.get(\"name\"),\n                                _actor(req))\n        except ValueError as e:\n            return 400, {\"ok\": False, \"error\": str(e)}\n    if method == \"market\":\n        return ctx.api.call(\"modsafe.market\")\n    if method == \"delete\":\n        body = req.get(\"body\") or {}\n        try:\n            return ctx.api.call(\"modsafe.delete\", body.get(\"name\"),\n                                _actor(req))\n        except ValueError as e:\n            return 400, {\"ok\": False, \"error\": str(e)}\n    if method == \"pack\":\n        query = req.get(\"query\") or {}\n        try:\n            return ctx.api.call(\"modsafe.pack\", query.get(\"name\"),\n                                str(query.get(\"secrets\") or \"\") == \"1\")\n        except ValueError as e:\n            return 400, {\"ok\": False, \"error\": str(e)}\n    if method == \"env_keys\":\n        query = req.get(\"query\") or {}\n        try:\n            return ctx.api.call(\"modsafe.env_keys\", query.get(\"name\"))\n        except ValueError as e:\n            return 400, {\"ok\": False, \"error\": str(e)}\n    if method == \"env_set\":\n        body = req.get(\"body\") or {}\n        if not isinstance(body.get(\"values\"), dict):\n            return 400, {\"ok\": False, \"error\": \"values должен быть объектом\"}\n        try:\n            return ctx.api.call(\"modsafe.env_set\",\n                                body.get(\"name\"), body.get(\"values\"),\n                                _actor(req))\n        except ValueError as e:\n            return 400, {\"ok\": False, \"error\": str(e)}\n    if method == \"inspect\":\n        query = req.get(\"query\") or {}\n        try:\n            return ctx.api.call(\"modsafe.inspect\", query.get(\"name\") or \"\")\n        except ValueError as e:\n            return 400, {\"ok\": False, \"error\": str(e)}\n    if method == \"orphans\":\n        if req.get(\"http_method\") == \"GET\":\n            return ctx.api.call(\"modsafe.orphans\")\n        body = req.get(\"body\") or {}\n        try:\n            return ctx.api.call(\"modsafe.uninstall_orphans\",\n                                body.get(\"packages\") or [])\n        except ValueError as e:\n            return 400, {\"ok\": False, \"error\": str(e)}\n    raise UserError(\"неизвестный метод: %s\" % method)\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"modsafe.ui_web — API кабинета + карточка «Модули и безопасность».\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ROUTES</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Модули: статус, аудит, хеши\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/enable\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"enable\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Вкл/выкл модуля {name, enabled} (применится после рестарта)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/rescan\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"rescan\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Обновить эталон хешей {name?}\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/upload\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"upload\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Загрузить архив модуля (multipart file=*.zip) в стейджинг\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/staged\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"staged\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Стейджинг загруженных модулей\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/install\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"install\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Установить модуль из стейджинга </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{name}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (выключенным)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/market\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"market\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Маркет модулей (заглушка до запуска страницы)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/delete\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"delete\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Удалить модуль </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{name}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (только выключенный)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/pack\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"pack\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Скачать модуль zip (?name, ?secrets=1)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/env\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"env_keys\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ключи секретов модуля (?name, без значений)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/env\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"env_set\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Сохранить секреты модуля {name, values}\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/inspect\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"inspect\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Паспорт модуля (?name): манифест, API, права, вклады, README\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/orphans\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"orphans\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"pip-сироты: пакеты, которых не требует никто (только показ)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/orphans\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"uninstall_orphans\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Удалить пакеты-сироты </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{packages</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> [...]}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (с перепроверкой)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">SERVICE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"icon\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🧩\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Модули и безопасность\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">           \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modules_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"phase\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">           \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Вкл/выкл модулей, аудит кода, сверка хешей\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">PANELS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _actor</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(req):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    sess </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'session'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> 'web:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sess.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'uid'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sess.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'uid'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '?'</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> handle_api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx, config, method, req):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.errors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        query </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"query\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> query.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"findings\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe.audit_one\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, query.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"findings\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe.list\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"enable\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe.set_enabled\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                                body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"enabled\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                                _actor(req))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"rescan\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe.rescan\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"upload\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        files </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"files\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}).get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"file\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> files:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Файл не передан (multipart file=*.zip)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        fi </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> files[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe.stage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, fi.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"filename\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"upload.zip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                                fi.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"content_type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, fi.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> b</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"staged\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe.staged\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"install\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe.install\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                                _actor(req))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"market\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe.market\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"delete\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe.delete\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                                _actor(req))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"pack\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        query </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"query\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe.pack\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, query.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                                str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(query.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"secrets\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"1\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"env_keys\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        query </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"query\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe.env_keys\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, query.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"env_set\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"values\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"values должен быть объектом\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe.env_set\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                                body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"values\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                                _actor(req))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"inspect\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        query </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"query\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe.inspect\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, query.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"orphans\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"http_method\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe.orphans\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"modsafe.uninstall_orphans\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                                body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"packages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"неизвестный метод: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method)</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "web/view.html",
          "size": 2571,
          "content": "<!-- Модульная вьюха modsafe. -->\n<section id=\"view-modsafe\" class=\"hidden\">\n  <h2>Модули и безопасность</h2>\n  <p class=\"muted\">Вкл/выкл применяется после рестарта бота. Новые модули появляются выключенными. Эталон хешей обновляйте только после проверки находок.</p>\n  <div class=\"table-wrap\"><table id=\"ms-table\"><thead><tr><th>Модуль</th><th>Статус</th><th>Аудит</th><th>Хеши</th><th>Действие</th></tr></thead><tbody></tbody></table></div>\n  <p class=\"muted\" id=\"ms-info\"></p>\n  <div class=\"card\" style=\"margin-top:12px\">\n    <h3>Находки</h3>\n    <div class=\"mono\" id=\"ms-findings\" style=\"word-break:break-word;white-space:pre-wrap;max-height:400px;overflow:auto;font-size:12px\"><span class=\"muted\">выберите модуль (кнопка 🔍)</span></div>\n  </div>\n  <div class=\"card\" style=\"margin-top:12px\">\n    <h3>Установка из архива</h3>\n    <p class=\"muted\">Только <code>.zip</code> / <code>.tar.gz</code> с <code>manifest.yaml</code> в корне. Модуль попадёт в стейджинг и пройдёт аудит; установка — выключенным, включение вручную.</p>\n    <div class=\"toolbar\" style=\"margin-bottom:8px\">\n      <input type=\"file\" id=\"ms-file\" accept=\".zip,.tar.gz,.tgz,.tar\" style=\"flex:1;min-width:160px\">\n      <button class=\"btn-mini\" onclick=\"msUpload(this)\">Загрузить</button>\n    </div>\n    <div class=\"mono\" id=\"ms-upload-out\" style=\"word-break:break-word;white-space:pre-wrap;font-size:12px\"></div>\n    <div id=\"ms-staged\" style=\"margin-top:8px\"></div>\n  </div>\n  <div class=\"card\" style=\"margin-top:12px\">\n    <h3>Маркет</h3>\n    <div class=\"muted\" id=\"ms-market\">проверка…</div>\n  </div>\n  <div class=\"card\" style=\"margin-top:12px\">\n    <h3>Сироты pip</h3>\n    <p class=\"muted\">Пакеты в venv, которых не требует ни один установленный модуль (удаление модуля пакеты не сносит — общие). Только показ; удаление — вручную из консоли.</p>\n    <div class=\"toolbar\" style=\"margin-bottom:8px\">\n      <button class=\"btn-mini\" onclick=\"msOrphans(this)\">Проверить</button>\n    </div>\n    <div class=\"mono\" id=\"ms-orphans\" style=\"font-size:12px\"><span class=\"muted\">не проверялось</span></div>\n  </div>\n</section>\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">&#x3C;!-- Модульная вьюха modsafe. --></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">section</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"view-modsafe\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Модули и безопасность&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Вкл/выкл применяется после рестарта бота. Новые модули появляются выключенными. Эталон хешей обновляйте только после проверки находок.&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"table-wrap\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">table</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ms-table\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">thead</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">tr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Модуль&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Статус&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Аудит&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Хеши&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Действие&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">tr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">thead</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">tbody</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">tbody</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">table</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ms-info\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"card\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"margin-top:12px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Находки&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ms-findings\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"word-break:break-word;white-space:pre-wrap;max-height:400px;overflow:auto;font-size:12px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>выберите модуль (кнопка 🔍)&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"card\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"margin-top:12px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Установка из архива&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Только &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>.zip&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">> / &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>.tar.gz&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">> с &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>manifest.yaml&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">> в корне. Модуль попадёт в стейджинг и пройдёт аудит; установка — выключенным, включение вручную.&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toolbar\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"margin-bottom:8px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">input</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> type</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"file\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ms-file\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> accept</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".zip,.tar.gz,.tgz,.tar\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"flex:1;min-width:160px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> onclick</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">msUpload</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">this</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Загрузить&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ms-upload-out\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"word-break:break-word;white-space:pre-wrap;font-size:12px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ms-staged\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"margin-top:8px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"card\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"margin-top:12px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Маркет&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ms-market\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>проверка…&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"card\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"margin-top:12px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Сироты pip&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Пакеты в venv, которых не требует ни один установленный модуль (удаление модуля пакеты не сносит — общие). Только показ; удаление — вручную из консоли.&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toolbar\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"margin-bottom:8px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> onclick</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">msOrphans</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">this</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Проверить&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ms-orphans\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"font-size:12px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>не проверялось&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">section</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "web/view.js",
          "size": 21523,
          "content": "/* modsafe — модульная вьюха (modules/modsafe/web/view.js). */\n/* Модули и безопасность (modsafe) */\nfunction msAuditPill(s) {\n  s = s || {};\n  const c = s.critical || 0, h = s.high || 0, m = s.medium || 0, l = s.low || 0;\n  if (c) return `<span class=\"pill off\">crit ${c}</span>`;\n  if (h) return `<span class=\"pill off\">high ${h}</span>`;\n  if (m || l) return `<span class=\"pill on\">med ${m} · low ${l}</span>`;\n  return `<span class=\"pill on\">чисто</span>`;\n}\n\nasync function loadModsafe() {\n  const tbody = document.querySelector(\"#ms-table tbody\");\n  const info = document.querySelector(\"#ms-info\");\n  tbody.innerHTML = `<tr><td colspan=\"5\" class=\"muted\">загрузка…</td></tr>`;\n  msStaged();\n  msMarket();\n  const r = await api(\"/api/modsafe/modules\");\n  if (!r.ok) { tbody.innerHTML = `<tr><td colspan=\"5\" class=\"muted\">Ошибка: ${esc((r.data && r.data.error) || \"\")}</td></tr>`; return; }\n  const mods = r.data.modules || [];\n  const off = mods.filter((x) => !x.enabled && !x.transport).length;\n  const bad = mods.filter((x) => !(x.hashes || {}).ok).length;\n  info.textContent = `всего: ${mods.length} · выключено: ${off} · хеши изменены: ${bad}`;\n  const CATS = [[\"core\", \"Ядро — без них система не работает\"], [\"base\", \"Базовые — управление сервисом\"],\n    [\"extra\", \"Дополнительные\"], [\"dev\", \"Служебные\"]];\n  const byCat = {};\n  for (const x of mods) { const c = x.category || \"extra\"; (byCat[c] = byCat[c] || []).push(x); }\n  const row = (x) => {\n    const s = x.summary || {}, h = x.hashes || {};\n    const st = x.transport ? `<span class=\"pill on\">транспорт</span>`\n      : (x.enabled ? `<span class=\"pill on\">вкл</span>` : `<span class=\"pill off\">выкл</span>`);\n    const hs = h.ok ? `<span class=\"pill on\">ok</span>`\n      : `<span class=\"pill off\" title=\"${esc([...(h.changed || []), ...(h.new || []), ...(h.missing || [])].join(\", \"))}\">DIFF</span>`;\n    const canToggle = !x.transport && x.name !== \"modsafe\" && (x.category || \"\") !== \"core\";\n    return `<tr>\n      <td><span class=\"mono\">${esc(x.name)}</span> <span class=\"muted\" style=\"font-size:11px\">${esc(x.version || \"\")}</span><br><span class=\"muted\" style=\"font-size:11px\">${esc((x.description || \"\").slice(0, 90))}</span></td>\n      <td>${st}</td>\n      <td>${msAuditPill(s)}</td>\n      <td>${hs}</td>\n      <td class=\"actions\"><div style=\"display:flex;gap:4px;flex-wrap:wrap;align-items:center\">\n        <button class=\"btn-mini\" title=\"Показать находки аудита\" onclick=\"msFindings(${escJs(x.name)})\">🔍</button>\n        <button class=\"btn-mini\" title=\"Паспорт модуля: README, права, API, вклады\" onclick=\"msInspect(${escJs(x.name)})\">📋</button>\n        ${canToggle ? `<button class=\"btn-mini\" title=\"${x.enabled ? \"Выключить модуль (применится после рестарта)\" : \"Включить модуль (применится после рестарта)\"}\" onclick=\"msToggle(${escJs(x.name)},${x.enabled ? \"false\" : \"true\"},this)\">${x.enabled ? \"Выкл\" : \"Вкл\"}</button>` : \"\"}\n        <button class=\"btn-mini\" title=\"Обновить эталон хешей (текущие файлы = доверенные)\" onclick=\"msRescan(${escJs(x.name)},this)\">♻️</button>\n        ${!x.transport && x.name !== \"modsafe\" ? `<button class=\"btn-mini\" title=\"Скачать архив модуля (без секретов)\" onclick=\"msPack(${escJs(x.name)},false,this)\">📦</button>\n        <button class=\"btn-mini\" title=\"Скачать архив ВМЕСТЕ с секретами (опасно!)\" onclick=\"msPack(${escJs(x.name)},true,this)\">📦🔑</button>\n        <button class=\"btn-mini\" title=\"Секреты модуля (.env)\" onclick=\"msEnv(${escJs(x.name)})\">🔑</button>\n        ${!x.enabled && (x.category || \"\") !== \"core\" ? `<button class=\"btn-mini danger\" title=\"Удалить модуль (только выключенный)\" onclick=\"msDelete(${escJs(x.name)},this)\">🗑</button>` : \"\"}` : \"\"}\n      </div></td>\n    </tr>`;\n  };\n  const isCore = (x) => (x.category || \"\") === \"core\";\n  tbody.innerHTML = CATS.map(([c, title]) => {\n    const items = (byCat[c] || []).filter((x) => !x.transport);\n    const trs = (byCat[c] || []).filter((x) => x.transport);\n    if (!items.length && !trs.length) return \"\";\n    let html = `<tr><td colspan=\"5\" class=\"muted\" style=\"padding-top:10px\"><b>${title}</b> (${items.length + trs.length})</td></tr>`;\n    // core без кнопок управления (locked), остальные как раньше\n    html += items.map((x) => row(x)).join(\"\");\n    html += trs.map((x) => row(x)).join(\"\");\n    return html;\n  }).join(\"\");\n}\n\nasync function msToggle(name, enable, btn) {\n  if (btn) btn.disabled = true;\n  const r = await api(\"/api/modsafe/enable\", { method: \"POST\", body: JSON.stringify({ name, enabled: enable }) });\n  if (btn) btn.disabled = false;\n  if (!r.ok) { showToast((r.data && r.data.error) || \"Ошибка\"); return; }\n  showToast(enable ? \"Включено (применится после рестарта)\" : \"Выключено (применится после рестарта)\");\n  loadModsafe();\n}\n\nasync function msRescan(name, btn) {\n  if (btn) btn.disabled = true;\n  const r = await api(\"/api/modsafe/rescan\", { method: \"POST\", body: JSON.stringify({ name }) });\n  if (btn) btn.disabled = false;\n  if (!r.ok) { showToast((r.data && r.data.error) || \"Ошибка\"); return; }\n  showToast(\"Эталон хешей обновлён\");\n  loadModsafe();\n}\n\nasync function msFindings(name) {\n  const box = document.querySelector(\"#ms-findings\");\n  box.innerHTML = `<span class=\"muted\">загрузка…</span>`;\n  const r = await api(\"/api/modsafe/modules?findings=\" + encodeURIComponent(name));\n  if (!r.ok) { box.innerHTML = `<span class=\"muted\">Ошибка: ${esc((r.data && r.data.error) || \"\")}</span>`; return; }\n  const fs = r.data.findings || [];\n  if (!fs.length) { box.innerHTML = `<b>${esc(name)}</b> — находок нет, чисто.`; return; }\n  box.innerHTML = `<b>${esc(name)}</b> — находок: ${fs.length}<br>` + fs.slice(0, 200).map((f) =>\n    `<span class=\"mono\">${esc(f.file)}:${f.line}</span> <b>${esc(f.pid)}</b> [${esc(f.severity)}/${esc(f.kind)}] ${esc(f.desc)}`\n  ).join(\"<br>\") + (fs.length > 200 ? `<br><span class=\"muted\">…и ещё ${fs.length - 200}</span>` : \"\");\n  box.scrollIntoView({ block: \"nearest\" });\n}\n\n/* Паспорт модуля: манифест, API, права, вклады, README. */\nasync function msInspect(name) {\n  const box = document.querySelector(\"#ms-findings\");\n  box.innerHTML = `<span class=\"muted\">загрузка…</span>`;\n  const r = await api(\"/api/modsafe/inspect?name=\" + encodeURIComponent(name));\n  if (!r.ok) { box.innerHTML = `<span class=\"muted\">Ошибка: ${esc((r.data && r.data.error) || \"\")}</span>`; return; }\n  const d = r.data || {};\n  const m = d.manifest || {};\n  const pill = (on, t) => `<span class=\"pill ${on ? \"on\" : \"off\"}\">${esc(t)}</span>`;\n  const tbl = (rows, cols) => rows.length ? `<div class=\"table-wrap\"><table><thead><tr>${cols.map((c) => `<th>${esc(c[1])}</th>`).join(\"\")}</tr></thead><tbody>` +\n    rows.map((x) => `<tr>${cols.map((c) => `<td${c[2] ? ` class=\"${c[2]}\"` : \"\"}>${esc(x[c[0]] ?? \"\")}</td>`).join(\"\")}</tr>`).join(\"\") +\n    `</tbody></table></div>` : `<span class=\"muted\">— нет —</span>`;\n  let md = d.readme || \"\";\n  try {\n    if (md && typeof marked !== \"undefined\" && typeof DOMPurify !== \"undefined\")\n      md = DOMPurify.sanitize(marked.parse(md));\n    else md = esc(md);\n  } catch { md = esc(d.readme || \"\"); }\n  const routes = (d.ui_web && d.ui_web.routes) || [];\n  const svcs = (d.ui_web && d.ui_web.service) || [];\n  const panels = (d.ui_web && d.ui_web.panels) || [];\n  const nav = (d.ui_web && d.ui_web.nav) || [];\n  box.innerHTML =\n    `<h3><span class=\"mono\">${esc(d.name)}</span> <span class=\"muted\">${esc(m.version || \"\")} · ${esc(m.category || \"\")}</span></h3>`\n    + `<div style=\"margin:4px 0\">${pill(d.loaded, d.loaded ? \"загружен\" : \"не загружен\")} ${pill(d.enabled, d.enabled ? \"включён\" : \"выключен\")}${d.transport ? ` ${pill(true, \"транспорт\")}` : \"\"}</div>`\n    + (m.description ? `<p>${esc(m.description)}</p>` : \"\")\n    + (m.requires && m.requires.length ? `<div><b>Зависимости:</b> <span class=\"mono\">${m.requires.map(esc).join(\", \")}</span></div>` : \"\")\n    + (m.uses_rights && m.uses_rights.length ? `<div><b>Чужие права:</b> <span class=\"mono\">${m.uses_rights.map(esc).join(\", \")}</span></div>` : \"\")\n    + `<h4>Права (${(d.rights || []).length})</h4>` + tbl(d.rights || [], [[\"name\", \"право\", \"mono\"], [\"description\", \"описание\"], [\"all_ignore\", \"игнор-all\"]])\n    + (Object.keys(d.entry_defaults || {}).length ? `<h4>Поля записей</h4><div class=\"mono\" style=\"font-size:12px\">${esc(JSON.stringify(d.entry_defaults))}</div>` : \"\")\n    + `<h4>API вживую (${(d.api_live || []).length})</h4>` + tbl(d.api_live || [], [[\"method\", \"метод\", \"mono\"], [\"doc\", \"что делает\"]])\n    + `<h4>Веб-роуты (${routes.length})</h4>` + tbl(routes.map((x) => Array.isArray(x) ? { m: x[0], p: x[1], k: x[2], r: (x[3] || {}).right || \"\" } : { m: x.http_method || x.method || \"\", p: x.path || \"\", k: x.name || \"\", r: x.right || \"\" }), [[\"m\", \"\"], [\"p\", \"путь\", \"mono\"], [\"k\", \"метод\"], [\"r\", \"право\"]])\n    + `<h4>Карточки кабинета (${svcs.length})</h4>` + tbl(svcs, [[\"key\", \"ключ\", \"mono\"], [\"title\", \"название\"], [\"right\", \"право\"]])\n    + (panels.length ? `<h4>Панели мониторинга (${panels.length})</h4>` + tbl(panels, [[\"key\", \"ключ\", \"mono\"], [\"title\", \"название\"]]) : \"\")\n    + (nav.length ? `<h4>Навигация (${nav.length})</h4>` + tbl(nav, [[\"id\", \"id\", \"mono\"], [\"title\", \"название\"]]) : \"\")\n    + ((d.tg_menu || []).length ? `<h4>TG-кнопки (${d.tg_menu.length})</h4>` + tbl(d.tg_menu, [[\"id\", \"id\", \"mono\"], [\"title\", \"название\"], [\"right\", \"право\"]]) : \"\")\n    + (d.ws && d.ws.path ? `<h4>WebSocket</h4><div class=\"mono\">${esc(d.ws.path)} <span class=\"muted\">${esc(d.ws.right || \"\")}</span></div>` : \"\")\n    + `<h4>Health</h4><div class=\"mono\" style=\"font-size:12px\">${esc(JSON.stringify(d.health ?? null))}</div>`\n    + `<h4>Файлы (${(d.files || []).length})</h4><details><summary class=\"muted\">показать список</summary><div class=\"mono\" style=\"font-size:12px\">${(d.files || []).map(esc).join(\"<br>\")}</div></details>`\n    + `<h4>README</h4><div class=\"md-body\">${md || `<span class=\"muted\">нет README</span>`}</div>`\n    + `<div style=\"margin-top:8px\"><button class=\"btn-mini\" onclick=\"msFindings(${escJs(name)})\">🔍 находки аудита</button></div>`;\n  box.scrollIntoView({ block: \"nearest\" });\n}\n\n/* modsafe: загрузка архивом + стейджинг + маркет-заглушка */\nasync function msUpload(btn) {\n  const inp = document.querySelector(\"#ms-file\");\n  const out = document.querySelector(\"#ms-upload-out\");\n  if (!inp || !inp.files || !inp.files.length) { showToast(\"Выберите файл\"); return; }\n  if (btn) btn.disabled = true;\n  out.innerHTML = `<span class=\"muted\">загрузка…</span>`;\n  const fd = new FormData();\n  fd.append(\"file\", inp.files[0]);\n  let r;\n  try {\n    const res = await fetch(\"/api/modsafe/upload\", { method: \"POST\", body: fd });\n    r = { ok: res.ok, status: res.status, data: await res.json() };\n  } catch (e) {\n    r = { ok: false, data: { error: String(e) } };\n  }\n  if (btn) btn.disabled = false;\n  if (!r.ok) { out.innerHTML = `<span class=\"muted\">Ошибка: ${esc((r.data && r.data.error) || r.status)}</span>`; return; }\n  const d = r.data || {};\n  const s = d.summary || {};\n  out.innerHTML = `Застейджен <b>${esc(d.name || \"\")}</b> ${esc(d.version || \"\")}<br>`\n    + `находки: crit ${s.critical || 0} · high ${s.high || 0} · med ${s.medium || 0} · low ${s.low || 0}<br>`\n    + `<span class=\"muted\">Проверьте находки (🔍 в таблице после установки — сейчас модуль ещё не в списке; кратко: ${esc((d.findings || []).slice(0, 5).map((f) => f.pid + \"@\" + f.file).join(\", \") || \"чисто\")})</span>`;\n  inp.value = \"\";\n  msStaged();\n}\n\nasync function msStaged() {\n  const box = document.querySelector(\"#ms-staged\");\n  if (!box) return;\n  const r = await api(\"/api/modsafe/staged\");\n  if (!r.ok) { box.innerHTML = `<span class=\"muted\">Ошибка: ${esc((r.data && r.data.error) || \"\")}</span>`; return; }\n  const items = r.data.staged || [];\n  if (!items.length) { box.innerHTML = `<span class=\"muted\">стейджинг пуст</span>`; return; }\n  box.innerHTML = `<b>Стейджинг:</b><br>` + items.map((x) =>\n    `<span class=\"mono\">${esc(x.name)}</span> <span class=\"muted\">${esc(x.version || \"\")} ${esc((x.description || \"\").slice(0, 80))}</span> `\n    + `<button class=\"btn-mini\" title=\"Установить выключенным\" onclick=\"msInstall(${escJs(x.name)},this)\">Установить</button>`\n  ).join(\"<br>\");\n}\n\nasync function msInstall(name, btn) {\n  if (btn) btn.disabled = true;\n  const r = await api(\"/api/modsafe/install\", { method: \"POST\", body: JSON.stringify({ name }) });\n  if (btn) btn.disabled = false;\n  if (!r.ok) { showToast((r.data && r.data.error) || \"Ошибка\"); return; }\n  showToast(\"Установлен выключенным (включите в таблице + рестарт)\");\n  msStaged();\n  loadModsafe();\n}\n\nasync function msMarket() {\n  const box = document.querySelector(\"#ms-market\");\n  if (!box) return;\n  const r = await api(\"/api/modsafe/market\");\n  const d = (r.ok && r.data) || {};\n  box.innerHTML = `${esc(d.note || \"Маркет недоступен\")}${d.url ? `<br><span class=\"muted\">${esc(d.url)}</span>` : \"\"}`;\n}\n\n/* modsafe: удаление / упаковка / секреты */\nasync function msDelete(name, btn) {\n  if (!confirm(`Удалить модуль ${name} ? Каталог и запись будут стёрты.`)) return;\n  if (btn) btn.disabled = true;\n  const r = await api(\"/api/modsafe/delete\", { method: \"POST\", body: JSON.stringify({ name }) });\n  if (btn) btn.disabled = false;\n  if (!r.ok) { showToast((r.data && r.data.error) || \"Ошибка\"); return; }\n  showToast(`Модуль ${name} удалён (рестарт не требуется)`);\n  loadModsafe();\n}\n\nasync function msPack(name, withSecrets, btn) {\n  if (withSecrets && !confirm(`Скачать ${name} ВМЕСТЕ с секретами (.env)? Не пересылайте файл третьим лицам.`)) return;\n  if (btn) btn.disabled = true;\n  const r = await api(`/api/modsafe/pack?name=${encodeURIComponent(name)}${withSecrets ? \"&secrets=1\" : \"\"}`);\n  if (btn) btn.disabled = false;\n  if (!r.ok || !r.data.b64) { showToast((r.data && r.data.error) || \"Ошибка\"); return; }\n  const bin = atob(r.data.b64);\n  const buf = new Uint8Array(bin.length);\n  for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);\n  const url = URL.createObjectURL(new Blob([buf], { type: \"application/zip\" }));\n  const a = document.createElement(\"a\");\n  a.href = url;\n  a.download = r.data.filename || (name + \".zip\");\n  document.body.appendChild(a);\n  a.click();\n  setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 4000);\n  showToast(`Скачан ${a.download}${withSecrets ? \" (с секретами!)\" : \"\"}`);\n}\n\nasync function msEnv(name) {\n  const box = document.querySelector(\"#ms-findings\");\n  box.innerHTML = `<span class=\"muted\">загрузка…</span>`;\n  const r = await api(\"/api/modsafe/env?name=\" + encodeURIComponent(name));\n  if (!r.ok) { box.innerHTML = `<span class=\"muted\">Ошибка: ${esc((r.data && r.data.error) || \"\")}</span>`; return; }\n  const keys = r.data.keys || [];\n  if (!keys.length) { box.innerHTML = `<b>${esc(name)}</b> — схема секретов пуста.`; return; }\n  box.innerHTML = `<b>Секреты ${esc(name)}</b> <span class=\"muted\">(значения не показываются; пустое поле = не менять; применяются после рестарта)</span><br><br>`\n    + keys.map((k, i) => `<div style=\"margin-bottom:6px\"><span class=\"mono\">${esc(k.key)}</span> `\n    + (k.required ? `<b>*</b> ` : ``)\n    + (k.schema === false ? `<span class=\"pill on\" title=\"Ключа нет в схеме — динамический секрет из .env (например auth_ref)\">.env</span> ` : ``)\n    + `<span class=\"muted\" style=\"font-size:11px\">${(k.schema && !k.required && !k.set) ? \"конфиг, не секрет\" : (k.set ? \"задан\" : \"не задан\")}${k.default ? ` · дефолт: ${esc(k.default)}` : \"\"}</span><br>`\n    + `<input id=\"msenv-${i}\" data-key=\"${esc(k.key)}\" type=\"password\" autocomplete=\"new-password\" placeholder=\"новое значение (пусто = не менять)\" style=\"width:min(420px,90%)\"></div>`).join(\"\")\n    + `<div style=\"margin:8px 0 6px\"><span class=\"muted\" style=\"font-size:12px\">Новый ключ (для динамических секретов): </span><input id=\"msenv-new-key\" placeholder=\"ИМЯ_ПЕРЕМЕННОЙ\" style=\"width:180px\"> <input id=\"msenv-new-val\" type=\"password\" autocomplete=\"new-password\" placeholder=\"значение\" style=\"width:min(300px,60%)\"></div>`\n    + `<br><button class=\"btn-mini\" onclick=\"msEnvSave(${escJs(name)},this)\">Сохранить секреты</button> <span class=\"muted\" id=\"msenv-msg\"></span>`;\n  box.scrollIntoView({ block: \"nearest\" });\n}\n\nasync function msEnvSave(name, btn) {\n  const values = {};\n  document.querySelectorAll(\"#ms-findings input[data-key]\").forEach((inp) => {\n    if (inp.value) values[inp.dataset.key] = inp.value;\n  });\n  const nk = document.querySelector(\"#msenv-new-key\");\n  const nv = document.querySelector(\"#msenv-new-val\");\n  if (nk && nv && nv.value) {\n    const key = (nk.value || \"\").trim().toUpperCase();\n    if (!/^[A-Z][A-Z0-9_]{0,63}$/.test(key)) { showToast(\"Имя ключа: A-Z, цифры, _\"); return; }\n    values[key] = nv.value;\n  }\n  if (!Object.keys(values).length) { showToast(\"Нечего сохранять\"); return; }\n  if (btn) btn.disabled = true;\n  const r = await api(\"/api/modsafe/env\", { method: \"POST\", body: JSON.stringify({ name, values }) });\n  if (btn) btn.disabled = false;\n  const msg = document.querySelector(\"#msenv-msg\");\n  if (!r.ok) {\n    if (msg) msg.textContent = \"Ошибка: \" + ((r.data && r.data.error) || \"\");\n    else showToast((r.data && r.data.error) || \"Ошибка\");\n    return;\n  }\n  if (msg) msg.textContent = \"Сохранено: \" + ((r.data.changed || []).join(\", \") || \"без изменений\");\n  document.querySelectorAll(\"#ms-findings input[data-key]\").forEach((inp) => { inp.value = \"\"; });\n  showToast(\"Секреты сохранены (.env 0600)\");\n}\n\n/* Сироты pip: показ + готовая команда для консоли. */\nasync function msOrphans(btn) {\n  const box = document.querySelector(\"#ms-orphans\");\n  box.innerHTML = `<span class=\"muted\">считаю…</span>`;\n  if (btn) btn.disabled = true;\n  const r = await api(\"/api/modsafe/orphans\");\n  if (btn) btn.disabled = false;\n  if (!r.ok) { box.innerHTML = `<span class=\"muted\">Ошибка: ${esc((r.data && r.data.error) || \"\")}</span>`; return; }\n  const d = r.data || {};\n  const items = d.orphans || [];\n  if (!items.length) {\n    box.innerHTML = `<span class=\"muted\">сирот нет (пакетов: ${d.installed_n || \"?\"}, требуется: ${d.required_n || \"?\"})</span>`;\n    return;\n  }\n  const names = items.map((x) => x.name).join(\" \");\n  box.innerHTML = `Сироты (${items.length}):<br>`\n    + items.map((x) => `<span class=\"mono\">${esc(x.name)}==${esc(x.version)}</span>`).join(\"<br>\")\n    + `<br><br><button class=\"btn-mini danger\" data-pkgs=\"${esc(names)}\" onclick=\"msOrphansDel(this)\">Удалить сирот</button> <span class=\"muted\">тем же pip (с перепроверкой)</span>`\n    + `<div class=\"mono\" id=\"ms-orphans-out\" style=\"margin-top:6px\"></div>`;\n}\n\n/* Удаление сирот (confirm + перепроверка на сервере). */\nasync function msOrphansDel(btn) {\n  const out = document.querySelector(\"#ms-orphans-out\");\n  const names = ((btn && btn.dataset.pkgs) || \"\").split(\" \").filter(Boolean);\n  if (!names.length) { showToast(\"Нечего удалять\"); return; }\n  if (!confirm(`Удалить ${names.length} пакетов-сирот?\\n${names.join(\", \")}`)) return;\n  if (btn) btn.disabled = true;\n  const r = await api(\"/api/modsafe/orphans\", { method: \"POST\", body: JSON.stringify({ packages: names }) });\n  if (btn) btn.disabled = false;\n  if (!r.ok) { showToast((r.data && r.data.error) || \"Ошибка\"); return; }\n  const d = r.data || {};\n  if (out) out.textContent = `Удалено: ${(d.removed || []).join(\", \") || \"—\"}\\nПропущено: ${(d.skipped || []).map((s) => s.name + \" (\" + s.reason + \")\").join(\", \") || \"—\"}\\n${d.output || \"\"}`;\n  showToast(\"Готово — проверьте список заново\");\n  msOrphans(null);\n}\n\nwindow.BotUI.registerService('modsafe', 'modsafe');\nwindow.BotUI.registerView('modsafe', loadModsafe);\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* modsafe — модульная вьюха (modules/modsafe/web/view.js). */</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* Модули и безопасность (modsafe) */</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> msAuditPill</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">s</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  s </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> c</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s.critical </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">h</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s.high </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">m</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s.medium </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">l</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s.low </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (c) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"pill off\">crit ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">c</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (h) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"pill off\">high ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">h</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> l) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"pill on\">med ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} · low ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">l</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"pill on\">чисто&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> loadModsafe</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> tbody</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#ms-table tbody\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> info</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#ms-info\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  tbody.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;tr>&#x3C;td colspan=\"5\" class=\"muted\">загрузка…&#x3C;/td>&#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  msStaged</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  msMarket</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/modules\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { tbody.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;tr>&#x3C;td colspan=\"5\" class=\"muted\">Ошибка: ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">((</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">error</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td>&#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> mods</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.modules </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> off</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mods.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">filter</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">x</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x.enabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x.transport).</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> bad</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mods.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">filter</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">x</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(x.hashes </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}).ok).</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  info.textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `всего: ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mods</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} · выключено: ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">off</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} · хеши изменены: ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">bad</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> CATS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"core\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ядро — без них система не работает\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"base\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Базовые — управление сервисом\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"extra\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Дополнительные\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"dev\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Служебные\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]];</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> byCat</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> x</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> of</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mods) { </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> c</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x.category </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"extra\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; (byCat[c] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> byCat[c] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">push</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(x); }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> row</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">x</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> s</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x.summary </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">h</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x.hashes </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> st</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x.transport </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"pill on\">транспорт&#x3C;/span>`</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">      :</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (x.enabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"pill on\">вкл&#x3C;/span>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"pill off\">выкл&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> hs</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> h.ok </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"pill on\">ok&#x3C;/span>`</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">      :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"pill off\" title=\"${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">([</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">...</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">h</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">changed</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> []), </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">...</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">h</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">new</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> []), </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">...</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">h</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">missing</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> [])].</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\", \"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">))</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}\">DIFF&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> canToggle</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x.transport </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x.name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"modsafe\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (x.category </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"core\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;tr></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td>&#x3C;span class=\"mono\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span> &#x3C;span class=\"muted\" style=\"font-size:11px\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">version</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>&#x3C;br>&#x3C;span class=\"muted\" style=\"font-size:11px\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">((</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">description</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">slice</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">90</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">))</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td>${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">st</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td>${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">msAuditPill</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td>${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">hs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td class=\"actions\">&#x3C;div style=\"display:flex;gap:4px;flex-wrap:wrap;align-items:center\"></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        &#x3C;button class=\"btn-mini\" title=\"Показать находки аудита\" onclick=\"msFindings(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})\">🔍&#x3C;/button></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        &#x3C;button class=\"btn-mini\" title=\"Паспорт модуля: README, права, API, вклады\" onclick=\"msInspect(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})\">📋&#x3C;/button></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">canToggle</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;button class=\"btn-mini\" title=\"${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">enabled</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Выключить модуль (применится после рестарта)\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Включить модуль (применится после рестарта)\"}\" onclick=\"msToggle(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">},${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">enabled</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"false\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"true\"},this)\">${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">enabled</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Выкл\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Вкл\"}&#x3C;/button>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"}</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        &#x3C;button class=\"btn-mini\" title=\"Обновить эталон хешей (текущие файлы = доверенные)\" onclick=\"msRescan(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">},this)\">♻️&#x3C;/button></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        ${</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">transport</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"modsafe\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;button class=\"btn-mini\" title=\"Скачать архив модуля (без секретов)\" onclick=\"msPack(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">},false,this)\">📦&#x3C;/button></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        &#x3C;button class=\"btn-mini\" title=\"Скачать архив ВМЕСТЕ с секретами (опасно!)\" onclick=\"msPack(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">},true,this)\">📦🔑&#x3C;/button></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        &#x3C;button class=\"btn-mini\" title=\"Секреты модуля (.env)\" onclick=\"msEnv(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})\">🔑&#x3C;/button></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        ${</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">enabled</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">category</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"core\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;button class=\"btn-mini danger\" title=\"Удалить модуль (только выключенный)\" onclick=\"msDelete(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">},this)\">🗑&#x3C;/button>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"}`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"}</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;/div>&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    &#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  };</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> isCore</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">x</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (x.category </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"core\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  tbody.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> CATS</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(([</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">c</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">title</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> items</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (byCat[c] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">filter</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">x</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x.transport);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> trs</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (byCat[c] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">filter</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">x</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x.transport);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">items.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">trs.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    let</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> html </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;tr>&#x3C;td colspan=\"5\" class=\"muted\" style=\"padding-top:10px\">&#x3C;b>${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">title</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/b> (${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">items</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> trs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})&#x3C;/td>&#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    // core без кнопок управления (locked), остальные как раньше</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    html </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> items.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">x</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> row</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(x)).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    html </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> trs.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">x</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> row</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(x)).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> html;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> msToggle</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">enable</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/enable\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ name, enabled: enable }) });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(enable </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Включено (применится после рестарта)\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Выключено (применится после рестарта)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  loadModsafe</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> msRescan</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/rescan\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ name }) });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Эталон хешей обновлён\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  loadModsafe</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> msFindings</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> box</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#ms-findings\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\">загрузка…&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/modules?findings=\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> encodeURIComponent</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name));</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\">Ошибка: ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">((</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">error</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> fs</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.findings </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">fs.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;b>${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/b> — находок нет, чисто.`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;b>${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/b> — находок: ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">fs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;br>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fs.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">slice</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">200</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">f</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    `&#x3C;span class=\"mono\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">file</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}:${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">line</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span> &#x3C;b>${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">pid</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/b> [${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">severity</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}/${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">kind</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}] ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">desc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}`</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  ).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"&#x3C;br>\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (fs.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ></span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 200</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;br>&#x3C;span class=\"muted\">…и ещё ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">fs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> -</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 200</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  box.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">scrollIntoView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ block: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nearest\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> });</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* Паспорт модуля: манифест, API, права, вклады, README. */</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> msInspect</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> box</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#ms-findings\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\">загрузка…&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/inspect?name=\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> encodeURIComponent</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name));</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\">Ошибка: ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">((</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">error</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> d</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> m</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.manifest </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> pill</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">on</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">t</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"pill ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">on</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"on\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"off\"}\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">t</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tbl</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">rows</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">cols</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rows.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;div class=\"table-wrap\">&#x3C;table>&#x3C;thead>&#x3C;tr>${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">cols</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">((</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">c</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;th>${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">c</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">])</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/th>`</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/tr>&#x3C;/thead>&#x3C;tbody>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    rows.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">x</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;tr>${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">cols</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">((</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">c</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;td${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">c</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> ` class=\"${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">c</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">]</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}\"`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"}>${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">[</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">c</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">]] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">??</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td>`</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    `&#x3C;/tbody>&#x3C;/table>&#x3C;/div>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\">— нет —&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  let</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> md </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.readme </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (md </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> typeof</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> marked </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"undefined\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> typeof</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> DOMPurify </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"undefined\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      md </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> DOMPurify.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">sanitize</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(marked.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">parse</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(md));</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> md </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> esc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(md);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  } </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">catch</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { md </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> esc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(d.readme </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> routes</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (d.ui_web </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.ui_web.routes) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> svcs</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (d.ui_web </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.ui_web.service) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> panels</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (d.ui_web </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.ui_web.panels) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> nav</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (d.ui_web </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.ui_web.nav) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    `&#x3C;h3>&#x3C;span class=\"mono\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span> &#x3C;span class=\"muted\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">version</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} · ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">category</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>&#x3C;/h3>`</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;div style=\"margin:4px 0\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">pill</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">loaded</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">, </span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">loaded</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"загружен\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"не загружен\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">pill</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">enabled</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">, </span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">enabled</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"включён\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"выключен\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">transport</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> ` ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">pill</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">true</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"транспорт\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"}&#x3C;/div>`</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (m.description </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;p>${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">description</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/p>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (m.requires </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m.requires.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;div>&#x3C;b>Зависимости:&#x3C;/b> &#x3C;span class=\"mono\">${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">requires</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\", \"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>&#x3C;/div>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (m.uses_rights </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m.uses_rights.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;div>&#x3C;b>Чужие права:&#x3C;/b> &#x3C;span class=\"mono\">${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">uses_rights</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\", \"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>&#x3C;/div>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;h4>Права (${</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">rights</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> []).</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})&#x3C;/h4>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tbl</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(d.rights </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [], [[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"право\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"description\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"описание\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"all_ignore\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"игнор-all\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (Object.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">keys</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(d.entry_defaults </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}).</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;h4>Поля записей&#x3C;/h4>&#x3C;div class=\"mono\" style=\"font-size:12px\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">entry_defaults</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">))</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/div>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;h4>API вживую (${</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">api_live</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> []).</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})&#x3C;/h4>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tbl</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(d.api_live </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [], [[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"method\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"метод\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"doc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"что делает\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;h4>Веб-роуты (${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">routes</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})&#x3C;/h4>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tbl</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(routes.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">x</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Array.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">isArray</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(x) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { m: x[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], p: x[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], k: x[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], r: (x[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}).right </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> } </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { m: x.http_method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x.method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, p: x.path </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, k: x.name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, r: x.right </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> }), [[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"m\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"p\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"путь\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"k\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"метод\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"r\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"право\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;h4>Карточки кабинета (${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">svcs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})&#x3C;/h4>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tbl</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(svcs, [[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ключ\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"название\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"право\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (panels.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;h4>Панели мониторинга (${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">panels</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})&#x3C;/h4>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tbl</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(panels, [[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ключ\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"название\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]]) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (nav.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;h4>Навигация (${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">nav</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})&#x3C;/h4>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tbl</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(nav, [[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"название\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]]) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((d.tg_menu </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []).</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;h4>TG-кнопки (${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">tg_menu</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})&#x3C;/h4>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tbl</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(d.tg_menu, [[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"название\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"право\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]]) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (d.ws </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.ws.path </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;h4>WebSocket&#x3C;/h4>&#x3C;div class=\"mono\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ws</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">path</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} &#x3C;span class=\"muted\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ws</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">right</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>&#x3C;/div>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;h4>Health&#x3C;/h4>&#x3C;div class=\"mono\" style=\"font-size:12px\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">health</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ??</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">))</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/div>`</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;h4>Файлы (${</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">files</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> []).</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})&#x3C;/h4>&#x3C;details>&#x3C;summary class=\"muted\">показать список&#x3C;/summary>&#x3C;div class=\"mono\" style=\"font-size:12px\">${</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">files</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> []).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"&#x3C;br>\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/div>&#x3C;/details>`</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;h4>README&#x3C;/h4>&#x3C;div class=\"md-body\">${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">md</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\">нет README&#x3C;/span>`}&#x3C;/div>`</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;div style=\"margin-top:8px\">&#x3C;button class=\"btn-mini\" onclick=\"msFindings(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})\">🔍 находки аудита&#x3C;/button>&#x3C;/div>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  box.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">scrollIntoView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ block: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nearest\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> });</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* modsafe: загрузка архивом + стейджинг + маркет-заглушка */</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> msUpload</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> inp</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#ms-file\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> out</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#ms-upload-out\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">inp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">inp.files </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">inp.files.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Выберите файл\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  out.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\">загрузка…&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> fd</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> new</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> FormData</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  fd.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">append</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"file\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, inp.files[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  let</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> res</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> fetch</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/upload\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: fd });</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    r </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { ok: res.ok, status: res.status, data: </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">await</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> res.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">json</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() };</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  } </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">catch</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (e) {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    r </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { ok: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, data: { error: </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">String</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e) } };</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { out.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\">Ошибка: ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">((</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">error</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">status</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> d</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> s</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.summary </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  out.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `Застейджен &#x3C;b>${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/b> ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">version</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;br>`</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `находки: crit ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">critical</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} · high ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">high</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} · med ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">medium</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} · low ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">low</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;br>`</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\">Проверьте находки (🔍 в таблице после установки — сейчас модуль ещё не в списке; кратко: ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">((</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">findings</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> []).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">slice</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">((</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">pid</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"@\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">file</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\", \"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"чисто\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  inp.value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  msStaged</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> msStaged</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> box</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#ms-staged\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">box) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/staged\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\">Ошибка: ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">((</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">error</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> items</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.staged </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">items.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\">стейджинг пуст&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;b>Стейджинг:&#x3C;/b>&#x3C;br>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> items.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">x</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    `&#x3C;span class=\"mono\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span> &#x3C;span class=\"muted\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">version</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">((</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">description</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">slice</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">80</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">))</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span> `</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;button class=\"btn-mini\" title=\"Установить выключенным\" onclick=\"msInstall(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">},this)\">Установить&#x3C;/button>`</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  ).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"&#x3C;br>\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> msInstall</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/install\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ name }) });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Установлен выключенным (включите в таблице + рестарт)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  msStaged</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  loadModsafe</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> msMarket</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> box</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#ms-market\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">box) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/market\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> d</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (r.ok </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">note</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Маркет недоступен\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">url</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;br>&#x3C;span class=\"muted\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">url</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* modsafe: удаление / упаковка / секреты */</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> msDelete</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">confirm</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">`Удалить модуль ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} ? Каталог и запись будут стёрты.`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/delete\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ name }) });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">`Модуль ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} удалён (рестарт не требуется)`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  loadModsafe</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> msPack</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">withSecrets</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (withSecrets </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">confirm</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">`Скачать ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} ВМЕСТЕ с секретами (.env)? Не пересылайте файл третьим лицам.`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">`/api/modsafe/pack?name=${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">encodeURIComponent</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">withSecrets</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"&#x26;secrets=1\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.data.b64) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> bin</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> atob</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(r.data.b64);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> buf</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> new</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Uint8Array</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(bin.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">let</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> i </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; i </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> bin.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; i</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">++</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) buf[i] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> bin.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">charCodeAt</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(i);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> url</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> URL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">createObjectURL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">new</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Blob</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">([buf], { type: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"application/zip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> }));</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> a</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">createElement</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"a\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  a.href </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> url;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  a.download </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.filename </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \".zip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  document.body.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">appendChild</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(a);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  a.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">click</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  setTimeout</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">URL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">revokeObjectURL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(url); a.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">remove</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); }, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">4000</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">`Скачан ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">a</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">download</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">withSecrets</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" (с секретами!)\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> msEnv</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> box</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#ms-findings\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\">загрузка…&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/env?name=\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> encodeURIComponent</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name));</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\">Ошибка: ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">((</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">error</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> keys</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.keys </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">keys.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;b>${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/b> — схема секретов пуста.`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;b>Секреты ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/b> &#x3C;span class=\"muted\">(значения не показываются; пустое поле = не менять; применяются после рестарта)&#x3C;/span>&#x3C;br>&#x3C;br>`</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> keys.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">k</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">i</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;div style=\"margin-bottom:6px\">&#x3C;span class=\"mono\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">k</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">key</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span> `</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (k.required </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;b>*&#x3C;/b> `</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> ``</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (k.schema </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"pill on\" title=\"Ключа нет в схеме — динамический секрет из .env (например auth_ref)\">.env&#x3C;/span> `</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> ``</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\" style=\"font-size:11px\">${</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">k</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">schema</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">k</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">required</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">k</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">set</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"конфиг, не секрет\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">k</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">set</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"задан\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"не задан\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">k</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">default</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> ` · дефолт: ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">k</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">default</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"}&#x3C;/span>&#x3C;br>`</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;input id=\"msenv-${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">i</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}\" data-key=\"${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">k</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">key</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}\" type=\"password\" autocomplete=\"new-password\" placeholder=\"новое значение (пусто = не менять)\" style=\"width:min(420px,90%)\">&#x3C;/div>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;div style=\"margin:8px 0 6px\">&#x3C;span class=\"muted\" style=\"font-size:12px\">Новый ключ (для динамических секретов): &#x3C;/span>&#x3C;input id=\"msenv-new-key\" placeholder=\"ИМЯ_ПЕРЕМЕННОЙ\" style=\"width:180px\"> &#x3C;input id=\"msenv-new-val\" type=\"password\" autocomplete=\"new-password\" placeholder=\"значение\" style=\"width:min(300px,60%)\">&#x3C;/div>`</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;br>&#x3C;button class=\"btn-mini\" onclick=\"msEnvSave(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">},this)\">Сохранить секреты&#x3C;/button> &#x3C;span class=\"muted\" id=\"msenv-msg\">&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  box.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">scrollIntoView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ block: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nearest\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> });</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> msEnvSave</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> values</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelectorAll</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#ms-findings input[data-key]\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">forEach</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">inp</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (inp.value) values[inp.dataset.key] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> inp.value;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> nk</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msenv-new-key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> nv</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msenv-new-val\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (nk </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> nv </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> nv.value) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> key</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (nk.value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">trim</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">().</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">toUpperCase</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">^</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">[A-Z][A-Z0-9_]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">{0,63}$</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">test</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(key)) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Имя ключа: A-Z, цифры, _\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    values[key] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> nv.value;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Object.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">keys</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(values).</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Нечего сохранять\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/env\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ name, values }) });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> msg</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msenv-msg\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (msg) msg.textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Ошибка: \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    else</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (msg) msg.textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Сохранено: \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((r.data.changed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\", \"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"без изменений\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelectorAll</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#ms-findings input[data-key]\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">forEach</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">inp</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { inp.value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; });</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Секреты сохранены (.env 0600)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* Сироты pip: показ + готовая команда для консоли. */</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> msOrphans</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> box</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#ms-orphans\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\">считаю…&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/orphans\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\">Ошибка: ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">((</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">error</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> d</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> items</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.orphans </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">items.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"muted\">сирот нет (пакетов: ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">installed_n</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"?\"}, требуется: ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">required_n</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"?\"})&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> names</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> items.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">x</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x.name).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\" \"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  box.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `Сироты (${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">items</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}):&#x3C;br>`</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> items.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">x</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span class=\"mono\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}==${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">x</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">version</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"&#x3C;br>\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;br>&#x3C;br>&#x3C;button class=\"btn-mini danger\" data-pkgs=\"${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">names</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}\" onclick=\"msOrphansDel(this)\">Удалить сирот&#x3C;/button> &#x3C;span class=\"muted\">тем же pip (с перепроверкой)&#x3C;/span>`</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;div class=\"mono\" id=\"ms-orphans-out\" style=\"margin-top:6px\">&#x3C;/div>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* Удаление сирот (confirm + перепроверка на сервере). */</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> msOrphansDel</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> out</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#ms-orphans-out\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> names</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((btn </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> btn.dataset.pkgs) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">split</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\" \"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">filter</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Boolean);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">names.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Нечего удалять\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">confirm</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">`Удалить ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">names</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} пакетов-сирот?</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">names</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\", \"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/modsafe/orphans\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ packages: names }) });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> d</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (out) out.textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `Удалено: ${</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">removed</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> []).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\", \"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"—\"}</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Пропущено: ${</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">skipped</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> []).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">((</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" (\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">reason</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \")\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\", \"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"—\"}</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">output</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Готово — проверьте список заново\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  msOrphans</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">null</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">window.BotUI.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">registerService</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'modsafe'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'modsafe'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">window.BotUI.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">registerView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'modsafe'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, loadModsafe);</span></span>\n<span class=\"line\"></span></code></pre>"
        }
      ],
      "note": "Менеджер модулей бота",
      "_extra": {
        "api": "",
        "events_emitted": "[]",
        "events_subscribed": "[]",
        "audit_allow": "",
        "config_schema": "config.schema.json",
        "env_example": ".env.example"
      }
    },
    {
      "name": "admin",
      "version": "0.2.2",
      "category": "base",
      "description": "Админ-ядро TG (AdminMenu): главное меню, права, скрипты, журнал, бэкапы.",
      "requires": [],
      "rights": [],
      "api_methods": [],
      "web_routes": [],
      "commit": "2d7d04615b31be9f5bbd91d5d61671182164bf39",
      "updated": "2026-09-16 16:35:50 +0300",
      "integrity": "sha256:77902a4a084812eea8ee9419310a5a95957c43c134a8f010277cadbba20f4cea",
      "tree_url": "http://10.10.10.30:3000/euk0r/bot_modular/src/branch/main/modules/admin",
      "raw_url": "http://10.10.10.30:3000/euk0r/bot_modular/raw/branch/main/modules/admin/module.py",
      "files": [
        {
          "path": "README.md",
          "size": 4356,
          "content": "# admin `0.2.2`\n\nАдмин-ядро TG (AdminMenu): главное меню, права, скрипты, журнал, бэкапы. Две стороны: меню в Telegram и карточки в кабинете.\n\nТип: Модуль. Категория: `base`. Зависимостей нет.\n\n## Описание\n\n- В Telegram — главное меню `/start` с кнопками (администрирование, управление ботом, скрипты, уведомления, свой ID), видимость кнопок — по правам.\n- Управление: перезапуск службы бота, перезагрузка и выключение хоста; каждое действие под своим правом, пароль sudo — только из `.env`.\n- Админы: назначение и снятие TG-админов (пишет в `.env`, права сидит через `access`).\n- Скрипты: каталог `scripts_admin/` удалён, скрипты обслуживания теперь живут в `services/scripts` — список, запуск, просмотр и правка текста из кабинета.\n- Журнал: сообщения пользователей боту (включая группы) — кто, когда, что писал; поиск и фильтры в кабинете.\n- Раскладка кнопок `/start` (порядок, ширина, скрытие) правится в кабинете без правки кода.\n\n## Возможности\n\n- `admin.menu {}` → движок AdminMenu (тот же объект, что обслуживает кнопки TG)\n- `admin.toggle_right {key: 'telegram|vk|web:<id>', right}` → переключить право, вернуть новое значение\n- `admin.journal {limit=200}` → хвост журнала сообщений (кто/что/когда, включая группы)\n\n## Права\n\n- `restart_bot` — Перезапуск службы бота (POST /api/admin/system action=restart_bot)\n- `restart_machine` — Перезагрузка хоста (POST /api/admin/system action=restart_machine)\n- `shutdown_host` — Выключение хоста (POST /api/admin/system action=shutdown)\n- `manage_admins` — Назначение/снятие TG-админов (POST /api/admin/admins)\n- `receive_notifications_new_user` — Уведомления о новых пользователях (модуль admin)\n- `receive_notifications_new_user_edit` — Право менять себе уведомления о новых пользователях\n- `run_scripts` — Запуск скриптов обслуживания (GET/POST /api/admin/scripts)\n- `view_user_messages` — Журнал сообщений пользователей (GET /api/admin/messages)\n- `menu_edit` — Группы Telegram (TG-команды group*, /api/admin/group*)\n\n## Telegram\n\n- `/start` — главное меню (кнопки по правам)\n- `/id` — свой TG id (нужен для выдачи прав)\n\n## Web\n\n- `POST /api/admin/admins` — назначение/снятие админов\n- `POST /api/admin/system` — restart_bot / restart_machine / shutdown\n- `GET /api/admin/scripts`, `POST /api/admin/scripts` — список и запуск скриптов из `services/scripts`\n- `GET /api/admin/messages` — журнал сообщений с поиском\n- `GET /api/admin/group`, `POST /api/admin/group`, `GET /api/admin/group/messages` — группы Telegram\n- `GET /api/admin/tgmenu`, `POST /api/admin/tgmenu` — раскладка кнопок /start\n- Карточки кабинета: «Система», «Скрипты», «Сообщения», «Меню /start»\n\n## Конфигурация (`config.schema.json`)\n\n- Обязательных нет.\n- Опциональные (дефолты): `PAGE_SIZE` = `20`, `LOG_CONSOLE_MAX_MB` = `50`; остальные пусты по умолчанию: `ADMIN_PASS`, `CACHE_USER_MESSAGES`, `CACHE_FILE_NETWORK`, `SCAN_NETWORK_IGNORED_MACS`, `LOG_CONSOLE`, `BACKUP_DIR`, `F2B_CACHE`, `SYSTEMD_SERVICE`, `REMOTE_PASSWORD_WIN`, `REMOTE_PASSWORD_DOCKER`, `LOCAL_PASSWORD`, `REMOTE_HOST_WIN`, `REMOTE_USER_WIN`, `REMOTE_HOST_DOCKER`, `REMOTE_USER_DOCKER`\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\"># admin </span><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">`0.2.2`</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Админ-ядро TG (AdminMenu): главное меню, права, скрипты, журнал, бэкапы. Две стороны: меню в Telegram и карточки в кабинете.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Тип: Модуль. Категория: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`base`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">. Зависимостей нет.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Описание</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> В Telegram — главное меню </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`/start`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> с кнопками (администрирование, управление ботом, скрипты, уведомления, свой ID), видимость кнопок — по правам.</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Управление: перезапуск службы бота, перезагрузка и выключение хоста; каждое действие под своим правом, пароль sudo — только из </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`.env`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Админы: назначение и снятие TG-админов (пишет в </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`.env`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, права сидит через </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`access`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Скрипты: каталог </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`scripts_admin/`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> удалён, скрипты обслуживания теперь живут в </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`services/scripts`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — список, запуск, просмотр и правка текста из кабинета.</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Журнал: сообщения пользователей боту (включая группы) — кто, когда, что писал; поиск и фильтры в кабинете.</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Раскладка кнопок </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`/start`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (порядок, ширина, скрытие) правится в кабинете без правки кода.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Возможности</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `admin.menu {}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → движок AdminMenu (тот же объект, что обслуживает кнопки TG)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `admin.toggle_right {key: 'telegram|vk|web:&#x3C;id>', right}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → переключить право, вернуть новое значение</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `admin.journal {limit=200}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → хвост журнала сообщений (кто/что/когда, включая группы)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Права</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `restart_bot`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — Перезапуск службы бота (POST /api/admin/system action=restart_bot)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `restart_machine`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — Перезагрузка хоста (POST /api/admin/system action=restart_machine)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `shutdown_host`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — Выключение хоста (POST /api/admin/system action=shutdown)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `manage_admins`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — Назначение/снятие TG-админов (POST /api/admin/admins)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `receive_notifications_new_user`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — Уведомления о новых пользователях (модуль admin)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `receive_notifications_new_user_edit`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — Право менять себе уведомления о новых пользователях</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `run_scripts`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — Запуск скриптов обслуживания (GET/POST /api/admin/scripts)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `view_user_messages`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — Журнал сообщений пользователей (GET /api/admin/messages)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `menu_edit`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — Группы Telegram (TG-команды group*, /api/admin/group*)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Telegram</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `/start`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — главное меню (кнопки по правам)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `/id`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — свой TG id (нужен для выдачи прав)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Web</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `POST /api/admin/admins`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — назначение/снятие админов</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `POST /api/admin/system`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — restart_bot / restart_machine / shutdown</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `GET /api/admin/scripts`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`POST /api/admin/scripts`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — список и запуск скриптов из </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`services/scripts`</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `GET /api/admin/messages`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — журнал сообщений с поиском</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `GET /api/admin/group`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`POST /api/admin/group`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`GET /api/admin/group/messages`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — группы Telegram</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `GET /api/admin/tgmenu`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`POST /api/admin/tgmenu`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — раскладка кнопок /start</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Карточки кабинета: «Система», «Скрипты», «Сообщения», «Меню /start»</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Конфигурация (</span><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">`config.schema.json`</span><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Обязательных нет.</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Опциональные (дефолты): </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`PAGE_SIZE`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> = </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`20`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`LOG_CONSOLE_MAX_MB`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> = </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`50`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; остальные пусты по умолчанию: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`ADMIN_PASS`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`CACHE_USER_MESSAGES`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`CACHE_FILE_NETWORK`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`SCAN_NETWORK_IGNORED_MACS`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`LOG_CONSOLE`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`BACKUP_DIR`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`F2B_CACHE`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`SYSTEMD_SERVICE`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`REMOTE_PASSWORD_WIN`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`REMOTE_PASSWORD_DOCKER`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`LOCAL_PASSWORD`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`REMOTE_HOST_WIN`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`REMOTE_USER_WIN`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`REMOTE_HOST_DOCKER`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`REMOTE_USER_DOCKER`</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "config.py",
          "size": 1042,
          "content": "\"\"\"Пакетный config-шим для admin engine (вендор admin_menu.py пишет config.X).\n\nЗначения ставит Module из ModuleConfig + абсолютные пути.\nПрава и ADMIN_IDS — только через модуль access\n(движок делегирует; дефолты — из реестра ядра).\n\"\"\"\nimport os\n\n_vals: dict = {}\n\n\ndef _int(values: dict, key: str, default: int) -> int:\n    try:\n        return int(str(values.get(key, default) or default))\n    except (ValueError, TypeError):\n        return default\n\n\ndef install(values: dict, files: dict) -> None:\n    _vals.clear()\n    _vals.update(values)\n    _vals.update(files)\n    _vals[\"PAGE_SIZE\"] = _int(values, \"PAGE_SIZE\", 20)\n    _vals[\"LOG_CONSOLE_MAX_MB\"] = _int(values, \"LOG_CONSOLE_MAX_MB\", 50)\n    _vals[\"ADMIN_PASS\"] = values.get(\"ADMIN_PASS\") or \"\"\n\n\ndef __getattr__(name: str):\n    try:\n        return _vals[name]\n    except KeyError:\n        raise AttributeError(\"admin.config: нет ключа %s\" % name) from None\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"Пакетный config-шим для admin engine (вендор admin_menu.py пишет config.X).</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Значения ставит Module из ModuleConfig + абсолютные пути.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Права и ADMIN_IDS — только через модуль access</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(движок делегирует; дефолты — из реестра ядра).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">_vals: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(values: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, key: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, default: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(values.get(key, default) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> default))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TypeError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> default</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> install</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(values: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, files: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _vals.clear()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _vals.update(values)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _vals.update(files)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _vals[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"PAGE_SIZE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _int(values, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"PAGE_SIZE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">20</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _vals[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"LOG_CONSOLE_MAX_MB\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _int(values, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"LOG_CONSOLE_MAX_MB\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">50</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _vals[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ADMIN_PASS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> values.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ADMIN_PASS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> __getattr__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _vals[name]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> KeyError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> AttributeError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"admin.config: нет ключа </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "config.schema.json",
          "size": 514,
          "content": "{\n  \"required\": [],\n  \"optional\": {\n    \"ADMIN_PASS\": \"\",\n    \"CACHE_USER_MESSAGES\": \"\",\n    \"CACHE_FILE_NETWORK\": \"\",\n    \"SCAN_NETWORK_IGNORED_MACS\": \"\",\n    \"LOG_CONSOLE\": \"\",\n    \"PAGE_SIZE\": \"20\",\n    \"LOG_CONSOLE_MAX_MB\": \"50\",\n    \"BACKUP_DIR\": \"\",\n    \"F2B_CACHE\": \"\",\n    \"SYSTEMD_SERVICE\": \"\",\n    \"REMOTE_PASSWORD_WIN\": \"\",\n    \"REMOTE_PASSWORD_DOCKER\": \"\",\n    \"LOCAL_PASSWORD\": \"\",\n    \"REMOTE_HOST_WIN\": \"\",\n    \"REMOTE_USER_WIN\": \"\",\n    \"REMOTE_HOST_DOCKER\": \"\",\n    \"REMOTE_USER_DOCKER\": \"\"\n  }\n}\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">{</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"required\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: [],</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"optional\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"ADMIN_PASS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"CACHE_USER_MESSAGES\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"CACHE_FILE_NETWORK\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"SCAN_NETWORK_IGNORED_MACS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"LOG_CONSOLE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"PAGE_SIZE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"20\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"LOG_CONSOLE_MAX_MB\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"50\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"BACKUP_DIR\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"F2B_CACHE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"SYSTEMD_SERVICE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"REMOTE_PASSWORD_WIN\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"REMOTE_PASSWORD_DOCKER\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"LOCAL_PASSWORD\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"REMOTE_HOST_WIN\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"REMOTE_USER_WIN\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"REMOTE_HOST_DOCKER\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"REMOTE_USER_DOCKER\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "engine.py",
          "size": 63382,
          "content": "import logging\nimport os\nimport re\nimport subprocess\nimport json\nimport time\nimport select\nimport copy\nimport threading\nfrom datetime import datetime, timedelta\nfrom telegram import BotCommand, BotCommandScopeChat, InlineKeyboardMarkup, InlineKeyboardButton\nfrom telegram.ext import CommandHandler, MessageHandler, Filters, CallbackQueryHandler, DispatcherHandlerStop\nfrom telegram.utils.helpers import escape_markdown\nfrom logging.handlers import RotatingFileHandler\nfrom . import config\nimport pexpect\nimport threading\n\nlogger = logging.getLogger(__name__)\n\n# Небольшой кэш прав администраторов (TTL) – снижает количество чтений файла с диска\n# при отрисовке меню, где get_admin_right вызывается десятки раз подряд.\n# Лок защищает файл от гонок: права теперь трогают и другие потоки (web-канал).\n_rights_lock = threading.RLock()\n_rights_cache = {\"data\": None, \"ts\": 0.0}\n_RIGHTS_TTL = 5.0\n\n# Пространство имён мессенджера для ключей прав. Telegram- и VK-пользователи\n# имеют независимые ID, поэтому ключи вида \"telegram:<id>\" / \"vk:<id>\".\n_RIGHTS_NS = \"telegram\"\n\ndef _access():\n    \"\"\"Модуль access (единый владелец ADMIN_IDS/прав). Лениво — без порядка загрузки.\"\"\"\n    from botmod_access import engine as _acc\n    return _acc\n\n\ndef _admin_ids():\n    try:\n        return _access().admin_ids()\n    except Exception:\n        return []\n\n\ndef _norm_key(uid):\n    \"\"\"Квалифицированный ключ прав. Идемпотентен: уже квалифицированный\n    (\"telegram:123\") возвращает как есть, сырой числовой id оборачивает.\"\"\"\n    uid_s = str(uid)\n    if \":\" in uid_s:\n        return uid_s\n    return f\"{_RIGHTS_NS}:{uid_s}\"\n\ndef _numeric_uid(uid):\n    \"\"\"Числовой id из квалифицированного ключа прав ('telegram:123' -> 123).\n    Возвращает None, если цифр в конце нет.\"\"\"\n    import re as _re\n    m = _re.search(r\"(\\d+)$\", str(uid))\n    return int(m.group(1)) if m else None\n\n\ndef load_admin_rights():\n    \"\"\"BOTMOD: живьём из access (без TTL-кеша и прямых чтений файла).\"\"\"\n    try:\n        return _access().load_rights()\n    except Exception:\n        return {}\n\n\ndef save_admin_rights(rights):\n    \"\"\"BOTMOD: дифф-запись через ядро (атомарно).\"\"\"\n    try:\n        _access().save_rights(rights or {})\n    except Exception as e:\n        logger.error(f\"Ошибка сохранения прав администраторов: {e}\")\n\n\ndef delete_message(context):\n    job = context.job\n    try:\n        context.bot.delete_message(chat_id=job.context[\"chat_id\"], message_id=job.context[\"message_id\"])\n    except Exception as e:\n        if \"Message to delete not found\" in str(e):\n            logger.debug(\"Сообщение для удаления не найдено, пропускаем.\")\n        else:\n            logger.error(f\"Ошибка при удалении сообщения: {e}\")\n\ndef flush_buffer(child):\n    \"\"\"\n    Читает доступные данные из буфера, чтобы очистить его перед выполнением новой команды.\n    \"\"\"\n    flushed = \"\"\n    try:\n        while True:\n            flushed += child.read_nonblocking(size=1024, timeout=0)\n    except (pexpect.exceptions.TIMEOUT, pexpect.exceptions.EOF):\n        pass\n    return flushed\n\n_console_handler = None\n\ndef log_console(user_id, text):\n    \"\"\"Записывает команды/вывод консольных сессий в отдельный файл (config.LOG_CONSOLE).\n    Файл ротируется по размеру, суммарный объём ограничен config.LOG_CONSOLE_MAX_MB.\"\"\"\n    global _console_handler\n    try:\n        log_path = config.LOG_CONSOLE\n        os.makedirs(os.path.dirname(log_path), exist_ok=True)\n        if _console_handler is None or _console_handler.baseFilename != os.path.abspath(log_path):\n            backup_count = 1\n            max_bytes = max(1024, int(config.LOG_CONSOLE_MAX_MB * 1024 * 1024) // (backup_count + 1))\n            _console_handler = RotatingFileHandler(\n                log_path, maxBytes=max_bytes, backupCount=backup_count, encoding=\"utf-8\"\n            )\n            _console_handler.setFormatter(logging.Formatter(\"%(asctime)s %(message)s\"))\n        timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')\n        _console_handler.emit(logging.LogRecord(\n            name=\"console_sessions\",\n            level=logging.INFO,\n            pathname=__file__,\n            lineno=0,\n            msg=f\"[{timestamp}] [user:{user_id}] {text}\",\n            args=(),\n            exc_info=None,\n        ))\n        _console_handler.flush()\n    except Exception as e:\n        logger.error(f\"Ошибка записи лога консоли: {e}\")\n\ndef format_online(seconds):\n    \"\"\"Форматирует секунды в человекочитаемый вид: «3д 04:12» / «1:05:33» / «00:42».\"\"\"\n    seconds = int(seconds or 0)\n    d, rem = divmod(seconds, 86400)\n    h, rem = divmod(rem, 3600)\n    m, s = divmod(rem, 60)\n    if d:\n        return f\"{d}д {h:02d}:{m:02d}\"\n    if h:\n        return f\"{h:02d}:{m:02d}:{s:02d}\"\n    return f\"{m:02d}:{s:02d}\"\n\n# --- Функционал кеширования сообщений и действий пользователей ---\ndef load_user_message_cache():\n    cache = {\"messages\": [], \"users\": []}\n    try:\n        if os.path.exists(config.CACHE_USER_MESSAGES):\n            with open(config.CACHE_USER_MESSAGES, \"r\", encoding=\"utf-8\") as f:\n                cache = json.load(f)\n        if \"messages\" not in cache:\n            cache[\"messages\"] = []\n        if \"users\" not in cache:\n            cache[\"users\"] = []\n        # Удаляем сообщения старше 30 дней\n        threshold = datetime.now() - timedelta(days=30)\n        cache[\"messages\"] = [msg for msg in cache[\"messages\"] if datetime.fromisoformat(msg[\"time\"]) >= threshold]\n    except Exception as e:\n        logger.error(f\"Ошибка загрузки кеша сообщений: {e}\")\n    return cache\n\ndef save_user_message_cache(cache):\n    try:\n        with open(config.CACHE_USER_MESSAGES, \"w\", encoding=\"utf-8\") as f:\n            json.dump(cache, f, indent=4)\n    except Exception as e:\n        logger.error(f\"Ошибка сохранения кеша сообщений: {e}\")\n# --- Конец блока кеширования ---\n\nclass AdminMenu:\n    def __init__(self, dispatcher):\n        self.dispatcher = dispatcher\n        # BOTMOD: список администраторов — только через access (_admin_ids()).\n        self.sudo_password = config.ADMIN_PASS  # Пароль sudo из переменной окружения ADMIN_PASS\n        self.check_and_update_admin_rights()\n        # Словарь для хранения последнего отправленного ботом сообщения по каждому чату\n        self.last_bot_message = {}\n        # Словарь активных консольных сессий: ключ – user_id, значение – объект (pexpect или subprocess)\n        self.console_sessions = {}\n    \n    # --- Логирование сообщений и действий пользователей ---\n    def log_user_message(self, user, text, context):\n        cache = load_user_message_cache()\n        new_user = user.id not in cache[\"users\"]\n        if new_user:\n            cache[\"users\"].append(user.id)\n            self.notify_new_user(user, context)\n        # определяем чат для группы/лички\n        chat = None\n        try:\n            chat = context.bot.get_me  # dummy to keep context ref\n            # эффективный чат из context\n            if hasattr(context, 'chat_data'):\n                pass\n        except Exception:\n            pass\n        # пробуем взять из update через context? fallback\n        chat_id = None\n        chat_type = \"private\"\n        chat_title = \"\"\n        try:\n            # context may have effective_chat in some calls, but here we use passed update? caller passes user+text+context, we need update\n            # попробуем достать из context.chat_data? не надёжно, поэтому требуем чтобы вызывающий передал chat\n            pass\n        except Exception:\n            pass\n        message_entry = {\n            \"user_id\": user.id,\n            \"name\": user.username if user.username else user.first_name,\n            \"text\": text,\n            \"time\": datetime.now().isoformat(),\n            \"chat_id\": None,\n            \"chat_type\": \"private\",\n            \"chat_title\": \"\",\n            \"is_group\": False\n        }\n        # попытаемся извлечь чат из контекста вызывающего — если в context есть _group_log_chat\n        try:\n            gc = getattr(context, '_group_log_chat', None)\n            if gc:\n                message_entry[\"chat_id\"] = str(gc.get(\"id\") or gc.get(\"chat_id\"))\n                message_entry[\"chat_type\"] = gc.get(\"type\") or \"private\"\n                message_entry[\"chat_title\"] = gc.get(\"title\") or \"\"\n                message_entry[\"is_group\"] = message_entry[\"chat_type\"] in (\"group\",\"supergroup\")\n        except Exception:\n            pass\n        cache[\"messages\"].append(message_entry)\n        save_user_message_cache(cache)\n\n    def log_user_message_with_chat(self, user, text, chat):\n        # прямой вызов с объектом чата (для групп)\n        cache = load_user_message_cache()\n        if user.id not in cache[\"users\"]:\n            cache[\"users\"].append(user.id)\n        entry = {\n            \"user_id\": user.id,\n            \"name\": user.username if user.username else user.first_name,\n            \"text\": text,\n            \"time\": datetime.now().isoformat(),\n            \"chat_id\": str(getattr(chat, 'id', '') or ''),\n            \"chat_type\": getattr(chat, 'type', 'private') or 'private',\n            \"chat_title\": getattr(chat, 'title', '') or '',\n            \"is_group\": getattr(chat, 'type', '') in ('group','supergroup')\n        }\n        cache[\"messages\"].append(entry)\n        save_user_message_cache(cache)\n\n    def notify_new_user(self, user, context):\n        \"\"\"\n        Отправляет уведомление о новом пользователе администраторам, у которых включены\n        и общее право receive_notifications, и подчинённое receive_notifications_new_user.\n        Если включено receive_notifications_no_sound – отправляет без звука.\n        \"\"\"\n        for admin_id in _admin_ids():\n            if (self.get_admin_right(admin_id, \"receive_notifications\") and \n                self.get_admin_right(admin_id, \"receive_notifications_new_user\")):\n                no_sound = self.get_admin_right(admin_id, \"receive_notifications_no_sound\")\n                try:\n                    self.dispatcher.bot.send_message(\n                        chat_id=admin_id,\n                        text=f\"Новый пользователь: ID: {user.id}, Имя: {user.username if user.username else user.first_name}\",\n                        disable_notification=no_sound\n                    )\n                except Exception as e:\n                    logger.error(f\"Ошибка отправки уведомления администратору {admin_id}: {e}\")\n    # --- Конец логирования ---\n\n    def check_and_update_admin_rights(self):\n        # BOTMOD: сидинг/чистка — access.seed(); pve_manage_hosts дотягивает pve-модуль.\n        try:\n            _access().seed()\n        except Exception as e:\n            logger.error(f\"Ошибка синхронизации прав: {e}\")\n\n    def get_admin_right(self, user_id, key):\n        try:\n            return _access().get_right(user_id, key)\n        except Exception:\n            return False\n\n    def toggle_admin_right(self, target_id, key):\n        try:\n            return _access().toggle_right(_norm_key(target_id), key)\n        except Exception as e:\n            logger.error(f\"Ошибка переключения права: {e}\")\n            return False\n\n    # Обработка уведомлений по добавлению/удалению администраторов.\n    def process_add_admin(self, update, context, admin_id_text):\n        chat_id = update.message.chat.id if update.message else update.callback_query.message.chat.id\n        user = update.effective_user\n        if not self.get_admin_right(user.id, \"manage_admins\"):\n            self.send_msg(chat_id, \"Доступ запрещён!\", context=context)\n            return\n        new_admin = admin_id_text.strip()\n        if not new_admin.isdigit():\n            self.send_msg(chat_id, \"Неверный формат ID.\", context=context)\n            return\n        try:\n            # BOTMOD: скрипт правил старый config.py — теперь access (.env + seed).\n            changed, output = _access().add_admin_id(new_admin)\n            self.send_msg(chat_id, f\"Результат добавления администратора:\\n{output}\", context=context)\n            notify_msg = (\n                f\"Внимание!\\nАдминистратор {new_admin} добавлен пользователем \"\n                f\"{user.username} ({user.id}).\\nДля вступления изменений перезапустите бота.\"\n            )\n            for admin in _admin_ids():\n                if admin != user.id:\n                    self.dispatcher.bot.send_message(chat_id=admin, text=notify_msg)\n        except Exception as e:\n            logger.error(f\"Ошибка при добавлении администратора: {e}\")\n            self.send_msg(chat_id, \"Ошибка при добавлении администратора.\", context=context)\n\n    def process_remove_admin(self, update, context, admin_id_text):\n        chat_id = update.message.chat.id if update.message else update.callback_query.message.chat.id\n        user = update.effective_user\n        if not self.get_admin_right(user.id, \"manage_admins\"):\n            self.send_msg(chat_id, \"Доступ запрещён!\", context=context)\n            return\n        rem_admin = admin_id_text.strip()\n        if not rem_admin.isdigit():\n            self.send_msg(chat_id, \"Неверный формат ID.\", context=context)\n            return\n        try:\n            # BOTMOD: см. выше — access вместо скрипта.\n            cfg_changed, _rights_changed = _access().remove_admin_id(rem_admin)\n            output = (f\"Администратор {rem_admin} удалён.\"\n                      + (\" (требуется перезапуск)\" if cfg_changed else \"\"))\n            self.send_msg(chat_id, f\"Результат удаления администратора:\\n{output}\", context=context)\n            notify_msg = (\n                f\"Внимание!\\nАдминистратор {rem_admin} удалён пользователем \"\n                f\"{user.username} ({user.id}).\\nДля вступления изменений перезапустите бота.\"\n            )\n            for admin in _admin_ids():\n                if admin != user.id:\n                    self.dispatcher.bot.send_message(chat_id=admin, text=notify_msg)\n        except Exception as e:\n            logger.error(f\"Ошибка при удалении администратора: {e}\")\n            self.send_msg(chat_id, \"Ошибка при удалении администратора.\", context=context)\n\n    # --- Xiaomi miIO ---\n    def send_msg(self, chat_id, text, reply_markup=None, context=None, auto_delete=True, parse_mode=None):\n        if chat_id in self.last_bot_message:\n            try:\n                self.dispatcher.bot.delete_message(chat_id, self.last_bot_message[chat_id])\n            except Exception as e:\n                if \"Message to delete not found\" in str(e):\n                    logger.debug(\"Сообщение для удаления не найдено, пропускаем.\")\n                else:\n                    logger.error(f\"Ошибка при удалении сообщения: {e}\")\n            self.last_bot_message.pop(chat_id, None)\n\n        msg = self.dispatcher.bot.send_message(\n            chat_id=chat_id, text=text, reply_markup=reply_markup, parse_mode=parse_mode\n        )\n        self.last_bot_message[chat_id] = msg.message_id\n\n        if auto_delete and context is not None:\n            self.schedule_deletion(context, chat_id, msg.message_id)\n        return msg\n\n    def schedule_deletion(self, context, chat_id, message_id, delay=60):\n        try:\n            context.job_queue.run_once(\n                delete_message, delay, context={\"chat_id\": chat_id, \"message_id\": message_id}\n            )\n        except Exception as e:\n            logger.error(f\"Ошибка планирования удаления: {e}\")\n\n    def register_handlers(self):\n        self.dispatcher.add_handler(CommandHandler(\"start\", self.start))\n        self.dispatcher.add_handler(CommandHandler(\"id\", self.get_my_id))\n        self.dispatcher.add_handler(CallbackQueryHandler(self.button))\n        self.dispatcher.add_handler(MessageHandler(Filters.text & ~Filters.command, self.handle_text))\n        self.set_commands()\n\n    def set_commands(self, user_id=None):\n        \"\"\"Устанавливает команды бота.\n\n        Глобально (user_id is None) — только базовые /start и /id, чтобы\n        пользователи без прав не видели привилегированные команды.\n        Для конкретного user_id — персональный набор через BotCommandScopeChat\n        с учётом прав (ai_chat, opencode_chat, bot_status и т.д.).\n        \"\"\"\n        # BOTMOD: глобальный список команд публикует transport_telegram\n        # (динамически из TG_COMMANDS модулей) — движок его не затирает.\n        # Персональный scope по правам — как в старом боте.\n        if user_id is None:\n            return\n        try:\n            commands = [\n                BotCommand(\"start\", \"Запуск меню\"),\n                BotCommand(\"id\", \"Получить свой ID\"),\n                BotCommand(\"groupid\", \"Узнать ID группы\"),\n            ]\n            if self.get_admin_right(user_id, \"ai_chat\"):\n                commands.append(BotCommand(\"ai\", \"Задать вопрос ИИ (без контекста)\"))\n                commands.append(BotCommand(\"aiclose\", \"Завершить разговор с ИИ\"))\n                commands.append(BotCommand(\"aimodel\", \"Выбор модели AI-чата\"))\n                commands.append(BotCommand(\"aithink\", \"Переключить thinking\"))\n            if self.get_admin_right(user_id, \"opencode_chat\"):\n                commands.append(BotCommand(\"oc\", \"Задать вопрос OpenCode (MCP)\"))\n                commands.append(BotCommand(\"ocreset\", \"Завершить разговор с OpenCode\"))\n                commands.append(BotCommand(\"model\", \"Выбор модели OpenCode\"))\n                commands.append(BotCommand(\"octhink\", \"Переключить thinking OC\"))\n            if self.get_admin_right(user_id, \"schedule_manager\"):\n                commands.append(BotCommand(\"ocsched\", \"Отложенные задачи\"))\n            if self.get_admin_right(user_id, \"bot_status\"):\n                commands.append(BotCommand(\"status\", \"Статус бота\"))\n            if self.get_admin_right(user_id, \"group_manage\"):\n                commands.append(BotCommand(\"groupadd\", \"Добавить группу\"))\n                commands.append(BotCommand(\"groupdel\", \"Удалить группу\"))\n                commands.append(BotCommand(\"grouptoggle\", \"Вкл/выкл группу\"))\n            # groupid уже публично выше\n            self.dispatcher.bot.set_my_commands(commands, scope=BotCommandScopeChat(user_id))\n        except Exception as e:\n            logger.error(f\"Ошибка при установке команд: {e}\")\n\n    def is_admin(self, user_id):\n        try:\n            return _access().is_admin(user_id)\n        except Exception:\n            return False\n\n    def show_main_menu(self, chat_id, user_id, context=None):\n        try:\n            provider = getattr(self, \"tgmenu_provider\", None)\n            spec = provider(user_id) if callable(provider) else None\n        except Exception:  # noqa: BLE001\n            spec = None\n        keyboard = []\n        if spec is not None:\n            # Динамическое меню: кнопки загруженных модулей (порядок/ширина/\n            # hidden — data/tg_menu.json, права уже отфильтрованы провайдером).\n            # Ширина — доли строки, ряд добирается жадно (auto = половина).\n            slots = {\"1/4\": 0.25, \"2/4\": 0.5, \"3/4\": 0.75,\n                     \"1/3\": 1.0 / 3.0, \"2/3\": 2.0 / 3.0}\n            row, fill = [], 0.0\n            for b in spec:\n                btn = InlineKeyboardButton(\n                    b.get(\"title\") or b.get(\"id\"), callback_data=b.get(\"callback\"))\n                w = b.get(\"width\") or \"auto\"\n                if w == \"full\":\n                    if row:\n                        keyboard.append(row)\n                        row, fill = [], 0.0\n                    keyboard.append([btn])\n                    continue\n                need = slots.get(w, 0.5)\n                if row and fill + need > 1.0 + 1e-9:\n                    keyboard.append(row)\n                    row, fill = [], 0.0\n                row.append(btn)\n                fill += need\n            if row:\n                keyboard.append(row)\n        reply_markup = InlineKeyboardMarkup(keyboard)\n        self.send_msg(chat_id, \"Главное меню:\", reply_markup=reply_markup, context=context)\n\n    def start(self, update, context):\n        if update.message:\n            try:\n                context.bot.delete_message(chat_id=update.message.chat.id,\n                                           message_id=update.message.message_id)\n            except Exception as e:\n                logger.error(f\"Ошибка удаления команды /start: {e}\")\n        msg = update.message if update.message is not None else update.callback_query.message\n        user = update.effective_user\n        user_id = user.id\n        self.set_commands(user_id=user_id)\n        if not self.is_admin(user_id):\n            keyboard = [[InlineKeyboardButton(\"Получить мой ID\", callback_data=\"my_id\")]]\n            reply_markup = InlineKeyboardMarkup(keyboard)\n            self.send_msg(msg.chat.id,\n                          \"Доступ к основному меню ограничен.\\nНо вы можете узнать свой ID:\",\n                          reply_markup=reply_markup, context=context)\n            return\n        self.show_main_menu(msg.chat.id, user_id, context)\n\n    def get_my_id(self, update, context):\n        if update.message:\n            try:\n                context.bot.delete_message(chat_id=update.message.chat.id,\n                                           message_id=update.message.message_id)\n            except Exception as e:\n                logger.error(f\"Ошибка удаления команды /id: {e}\")\n        user = update.effective_user\n        self.send_msg(\n            update.effective_chat.id,\n            f\"Ваш ID: {user.id}\",\n            context=context,\n            auto_delete=True\n        )\n\n    def shutdown_host(self, chat_id, context, user_id):\n        if not self.get_admin_right(user_id, \"shutdown_host\"):\n            self.send_msg(chat_id, \"🚫 Доступ запрещён!\", context=context)\n            return\n        try:\n            subprocess.run(\n                [\"sudo\", \"-S\", \"shutdown\", \"-h\", \"now\"],\n                input=self.sudo_password + \"\\n\",\n                text=True,\n                capture_output=True,\n                check=True\n            )\n            self.send_msg(chat_id, \"Хост выключается.\", context=context)\n        except Exception as e:\n            logger.error(f\"Ошибка при выключении хоста: {e}\")\n            self.send_msg(chat_id, \"Ошибка при выключении хоста.\", context=context)\n\n    def start_console_session(self, chat_id, context, user_id):\n        if not self.get_admin_right(user_id, \"console_session\"):\n            self.send_msg(chat_id, \"🚫 Доступ запрещён!\", context=context)\n            return\n        if user_id in self.console_sessions:\n            self.send_msg(chat_id, \"Консоль уже запущена. Для завершения введите #quitconsole.\", context=context)\n            return\n        try:\n            child = pexpect.spawn(\"/bin/bash\", encoding=\"utf-8\", echo=False, timeout=5)\n            child.sendline(\"export PS1='[PEXPECT]>'\")\n            child.expect_exact(\"[PEXPECT]>\")\n            self.console_sessions[user_id] = child\n            log_console(user_id, \"СЕССИЯ ЗАПУЩЕНА\")\n            self.send_msg(\n                chat_id,\n                \"Консоль запущена. Введите команды. Для завершения сессии введите #quitconsole.\",\n                context=context\n            )\n        except Exception as e:\n            logger.error(f\"Ошибка при запуске консоли: {e}\")\n            self.send_msg(chat_id, \"Ошибка при запуске консоли.\", context=context)\n\n    def process_console_command(self, chat_id, context, user_id, command):\n        child = self.console_sessions.get(user_id)\n        if not child:\n            self.send_msg(chat_id, \"Консольная сессия не активна.\", context=context)\n            return\n        if command.strip() == \"#quitconsole\":\n            try:\n                child.sendline(\"exit\")\n                child.expect(pexpect.EOF)\n            except Exception:\n                pass\n            del self.console_sessions[user_id]\n            log_console(user_id, \"СЕССИЯ ЗАВЕРШЕНА\")\n            self.send_msg(chat_id, \"Консоль завершена.\", context=context)\n            return\n        try:\n            flush_buffer(child)\n            marker = \"__END_OF_CMD__\"\n            composite_command = f\"{command}; echo {marker}\"\n            child.sendline(composite_command)\n            output = \"\"\n            while True:\n                index = child.expect([marker, r\"Are you sure you want to continue connecting\", pexpect.EOF, pexpect.TIMEOUT], timeout=10)\n                if index == 0:\n                    output += child.before\n                    break\n                elif index == 1:\n                    output += child.before\n                    child.sendline(\"yes\")\n                elif index in (2, 3):\n                    output += child.before\n                    break\n            if output.startswith(command):\n                output = output[len(command):].strip()\n            if not output:\n                output = \"Команда выполнена, без вывода.\"\n            log_console(user_id, f\"$ {command}\\n{output}\")\n            self.send_msg(chat_id, f\"Вывод команды:\\n{output}\", context=context)\n        except Exception as e:\n            logger.error(f\"Ошибка выполнения команды в консоли: {e}\")\n            self.send_msg(chat_id, \"Ошибка выполнения команды в консоли.\", context=context)\n\n    def show_admin_menu(self, chat_id, context, user_id):\n        if not self.get_admin_right(user_id, \"manage_admins\"):\n            self.send_msg(chat_id, \"🚫 Доступ запрещён!\", context=context)\n            return\n        keyboard = []\n        row = []\n        row.append(InlineKeyboardButton(\"➕ Добавить\", callback_data=\"add_admin\"))\n        row.append(InlineKeyboardButton(\"❌ Удалить\", callback_data=\"remove_admin\"))\n        keyboard.append(row)\n        keyboard.append([InlineKeyboardButton(\"🔙 Назад\", callback_data=\"main_menu\")])\n        reply_markup = InlineKeyboardMarkup(keyboard)\n        self.send_msg(chat_id, \"👮‍♂️ Меню администрирования:\", reply_markup=reply_markup, context=context)\n\n    def show_bot_menu(self, chat_id, user_id, context):\n        # Проверяем наличие прав для управления ботом\n        if not (self.get_admin_right(user_id, \"restart_bot\") or \n                self.get_admin_right(user_id, \"restart_machine\") or \n                self.get_admin_right(user_id, \"shutdown_host\") or \n                self.get_admin_right(user_id, \"view_user_messages\")):\n            self.send_msg(chat_id, \"🚫 Доступ запрещён!\", context=context)\n            return\n        keyboard = []\n        row = []\n        if self.get_admin_right(user_id, \"restart_bot\"):\n            row.append(InlineKeyboardButton(\"🤖 Перезапуск бота\", callback_data=\"restart_bot\"))\n        if self.get_admin_right(user_id, \"restart_machine\"):\n            row.append(InlineKeyboardButton(\"🔄 Перезагрузка\", callback_data=\"restart_machine\"))\n        if row:\n            keyboard.append(row)\n        row = []\n        if self.get_admin_right(user_id, \"shutdown_host\"):\n            row.append(InlineKeyboardButton(\"⏻ Выключить хост\", callback_data=\"shutdown_host\"))\n        if self.get_admin_right(user_id, \"view_user_messages\"):\n            row.append(InlineKeyboardButton(\"📩 Получить последние сообщения\", callback_data=\"get_latest_messages\"))\n        if row:\n            keyboard.append(row)\n        keyboard.append([InlineKeyboardButton(\"🔙 Назад\", callback_data=\"main_menu\")])\n        reply_markup = InlineKeyboardMarkup(keyboard)\n        self.send_msg(chat_id, \"🤖 Меню управления ботом:\", reply_markup=reply_markup, context=context)\n    \n    def _scripts_dir(self):\n        \"\"\"Каталог пользовательских скриптов: <cwd>/scripts, fallback —\n        каталог скриптов модуля services (BOTMOD).\"\"\"\n        d = os.path.join(os.getcwd(), \"scripts\")\n        if not os.path.isdir(d):\n            d = os.path.join(\n                os.path.dirname(os.path.abspath(__file__)),\n                \"..\", \"services\", \"scripts\")\n        return d\n\n    def show_script_menu(self, chat_id, context, user_id):\n        if not self.get_admin_right(user_id, \"run_scripts\"):\n            self.send_msg(chat_id, \"🚫 Доступ запрещён!\", context=context)\n            return\n        scripts_dir = self._scripts_dir()\n        try:\n            files = os.listdir(scripts_dir)\n        except Exception as e:\n            self.send_msg(chat_id, \"Ошибка доступа к каталогу скриптов.\", context=context)\n            return\n        script_files = [f for f in files if f.endswith(\".sh\")]\n        if not script_files:\n            self.send_msg(chat_id, \"Нет доступных скриптов.\", context=context)\n            return\n        keyboard = []\n        row = []\n        for script in sorted(script_files):\n            row.append(InlineKeyboardButton(f\"📄 {script}\", callback_data=f\"script_menu:{script}\"))\n            if len(row) == 2:\n                keyboard.append(row)\n                row = []\n        if row:\n            keyboard.append(row)\n        keyboard.append([InlineKeyboardButton(\"🔙 Назад\", callback_data=\"main_menu\")])\n        reply_markup = InlineKeyboardMarkup(keyboard)\n        self.send_msg(chat_id, \"📜 Выберите скрипт для выполнения:\", reply_markup=reply_markup, context=context)\n\n    def run_script_common(self, chat_id, context, folder, script_name, required_right, params=None, user_id=None):\n        if not self.get_admin_right(user_id, required_right):\n            self.send_msg(chat_id, \"🚫 Доступ запрещён!\", context=context)\n            return\n\n        # BOTMOD: скрипты — вендор пакета; относительный folder резолвится\n        # от директории пакета, а не от CWD процесса.\n        if os.path.isabs(folder):\n            script_path = os.path.join(folder, script_name)\n        else:\n            script_path = os.path.join(\n                os.path.dirname(os.path.abspath(__file__)), folder, script_name)\n        if not os.path.exists(script_path):\n            self.send_msg(chat_id, f\"❌ Скрипт {script_name} не найден.\", context=context)\n            return\n\n        cmd_list = [\"sudo\", \"-S\", \"bash\", script_path]\n        if params:\n            dangerous_pattern = re.compile(r\"[;&|`$()]\")\n            param_list = params.split()\n            for param in param_list:\n                if dangerous_pattern.search(param):\n                    self.send_msg(chat_id, f\"🚫 Недопустимый параметр: {param}\", context=context)\n                    return\n            cmd_list.extend(param_list)\n        try:\n            # Не поднимаем исключение при ненулевом коде возврата — проверяем вручную\n            result = subprocess.run(\n                cmd_list,\n                input=self.sudo_password + \"\\n\",\n                text=True,\n                capture_output=True\n            )\n            if result.returncode != 0:\n                raw_help_text = result.stdout.strip() or result.stderr.strip() or f\"Скрипт {script_name} завершился с ошибкой.\"\n                help_text = escape_markdown(raw_help_text, version=2)\n                self.send_msg(\n                    chat_id,\n                    f\"⚠️ Ошибка выполнения скрипта {escape_markdown(script_name, version=2)}:\\n```\\n{help_text}\\n```\",\n                    context=context,\n                    parse_mode=\"MarkdownV2\"\n                )\n            else:\n                output = result.stdout.strip() or f\"Скрипт {script_name} выполнен без вывода.\"\n                output_escaped = escape_markdown(output, version=2)\n                self.send_msg(\n                    chat_id,\n                    f\"📄 Вывод скрипта {escape_markdown(script_name, version=2)}:\\n```\\n{output_escaped}\\n```\",\n                    context=context,\n                    parse_mode=\"MarkdownV2\"\n                )\n        except Exception as e:\n            logger.error(f\"Ошибка run_script_common для {script_name}: {e}\")\n            err_text = escape_markdown(str(e), version=2)\n            self.send_msg(\n                chat_id,\n                f\"⚠️ Ошибка при выполнении скрипта {escape_markdown(script_name, version=2)}:\\n```\\n{err_text}\\n```\",\n                context=context,\n                parse_mode=\"MarkdownV2\"\n            )\n\n\n    def show_notifications_menu(self, chat_id, context, user_id):\n        # Доступ к меню уведомлений только при наличии общего права receive_notifications\n        if not self.get_admin_right(user_id, \"receive_notifications\"):\n            self.send_msg(chat_id, \"У вас нет общего права receive_notifications, поэтому меню уведомлений недоступно.\", context=context)\n            return\n        # Кнопка для переключения отправки уведомлений без звука\n        no_sound_status = \"✅\" if self.get_admin_right(user_id, \"receive_notifications_no_sound\") else \"❌\"\n        keyboard = [[InlineKeyboardButton(f\"Отправлять без звука {no_sound_status}\", callback_data=\"toggle_no_sound_notification\")]]\n        # Добавляем кнопки для подчинённых уведомлений, если у администратора есть соответствующие _edit права\n        subordinate_buttons = []\n        if self.get_admin_right(user_id, \"receive_notifications_nmap_edit\"):\n            stat = \"✅\" if self.get_admin_right(user_id, \"receive_notifications_nmap\") else \"❌\"\n            subordinate_buttons.append(InlineKeyboardButton(f\"Nmap {stat}\", callback_data=\"toggle_nmap_notification\"))\n        if self.get_admin_right(user_id, \"receive_notifications_guacamole_edit\"):\n            stat = \"✅\" if self.get_admin_right(user_id, \"receive_notifications_guacamole\") else \"❌\"\n            subordinate_buttons.append(InlineKeyboardButton(f\"Guacamole {stat}\", callback_data=\"toggle_guacamole_notification\"))\n        if self.get_admin_right(user_id, \"receive_notifications_f2b_edit\"):\n            stat = \"✅\" if self.get_admin_right(user_id, \"receive_notifications_f2b\") else \"❌\"\n            subordinate_buttons.append(InlineKeyboardButton(f\"Fail2Ban {stat}\", callback_data=\"toggle_f2b_notification\"))\n        if self.get_admin_right(user_id, \"receive_notifications_wireguard_edit\"):\n            stat = \"✅\" if self.get_admin_right(user_id, \"receive_notifications_wireguard\") else \"❌\"\n            subordinate_buttons.append(InlineKeyboardButton(f\"Wireguard {stat}\", callback_data=\"toggle_wireguard_notification\"))\n        if self.get_admin_right(user_id, \"receive_notifications_status_edit\"):\n            stat = \"✅\" if self.get_admin_right(user_id, \"receive_notifications_status\") else \"❌\"\n            subordinate_buttons.append(InlineKeyboardButton(f\"Status {stat}\", callback_data=\"toggle_status_notification\"))\n        if self.get_admin_right(user_id, \"receive_notifications_new_user_edit\"):\n            stat = \"✅\" if self.get_admin_right(user_id, \"receive_notifications_new_user\") else \"❌\"\n            subordinate_buttons.append(InlineKeyboardButton(f\"Новые пользователи {stat}\", callback_data=\"toggle_new_user_notification\"))\n        if self.get_admin_right(user_id, \"receive_notifications_zabbix_edit\"):\n            stat = \"✅\" if self.get_admin_right(user_id, \"receive_notifications_zabbix\") else \"❌\"\n            subordinate_buttons.append(InlineKeyboardButton(f\"Zabbix {stat}\", callback_data=\"toggle_zabbix_notification\"))\n        # Разбиваем кнопки по 2 в строке\n        if subordinate_buttons:\n            rows = [subordinate_buttons[i:i+2] for i in range(0, len(subordinate_buttons), 2)]\n            keyboard.extend(rows)\n        else:\n            keyboard.append([InlineKeyboardButton(\"Нет дополнительных уведомлений\", callback_data=\"noop\")])\n        keyboard.append([InlineKeyboardButton(\"🔙 Назад\", callback_data=\"main_menu\")])\n        reply_markup = InlineKeyboardMarkup(keyboard)\n        self.send_msg(chat_id, \"Меню уведомлений бота:\", reply_markup=reply_markup, context=context)\n\n    def show_latest_messages(self, chat_id, context, user_id):\n        if not self.get_admin_right(user_id, \"view_user_messages\"):\n            self.send_msg(chat_id, \"🚫 Доступ запрещён!\", context=context)\n            return\n        cache = load_user_message_cache()\n        messages = []\n        for msg in cache.get(\"messages\", []):\n            dt_str = msg[\"time\"][:19]\n            messages.append(f\"{dt_str} - ID: {msg['user_id']} - {msg['name']}: {msg['text']}\")\n        result = \"\\n\".join(messages)\n        while result and len(result) > 4096 and messages:\n            messages.pop(0)\n            result = \"\\n\".join(messages)\n        if not result:\n            result = \"Нет сообщений в кеше.\"\n        self.send_msg(chat_id, result, context=context)\n\n    def button(self, update, context):\n        query = update.callback_query\n        user = query.from_user\n        user_id = user.id\n        self.log_user_message(user, f\"[Callback] {query.data}\", context)\n        logger.info(\"AdminMenu: User %s (ID: %s) нажал кнопку с данными: %s\", user.username, user_id, query.data)\n        if not self.is_admin(user_id) and not query.data.startswith(\"my_id\"):\n            query.answer()\n            query.edit_message_text(\"🚫 Доступ запрещён!\")\n            return\n        query.answer()\n        data = query.data\n        chat_id = query.message.chat.id\n\n        if data == \"main_menu\":\n            self.show_main_menu(chat_id, user_id, context)\n        elif data == \"admin_menu\":\n            self.show_admin_menu(chat_id, context, user_id)\n        elif data == \"add_admin\":\n            context.chat_data[\"awaiting\"] = \"add_admin\"\n            self.send_msg(chat_id, \"Введите ID нового администратора:\", context=context)\n        elif data == \"remove_admin\":\n            context.chat_data[\"awaiting\"] = \"remove_admin\"\n            self.send_msg(chat_id, \"Введите ID администратора для удаления:\", context=context)\n        elif data == \"bot_menu\":\n            self.show_bot_menu(chat_id, user_id, context)\n        elif data == \"restart_bot\":\n            if not self.get_admin_right(user_id, \"restart_bot\"):\n                query.edit_message_text(\"🚫 Доступ запрещён!\")\n                return\n            # Не редактируем уже удалённое сообщение – удаляем меню и отправляем результат новой строкой\n            try:\n                self.dispatcher.bot.delete_message(chat_id, query.message.message_id)\n            except Exception as e:\n                logger.error(f\"Ошибка удаления меню: {e}\")\n            unit = (getattr(config, \"SYSTEMD_SERVICE\", \"\") or \"\").strip()\n            if unit:\n                # A: рестарт через systemd (полный, как раньше; юнит из конфига).\n                try:\n                    subprocess.run(\n                        [\"sudo\", \"-S\", \"systemctl\", \"restart\", unit],\n                        input=self.sudo_password + \"\\n\",\n                        text=True,\n                        capture_output=True,\n                        check=True\n                    )\n                    self.send_msg(chat_id, \"Бот перезапущен (systemd).\", context=context)\n                except Exception as e:\n                    logger.error(f\"Ошибка при перезапуске бота: {e}\")\n                    self.send_msg(chat_id, \"Ошибка перезапуска.\", context=context)\n            else:\n                # B: самоперезапуск execv (без sudo/systemd; сначала ответ).\n                try:\n                    from core import restart as _restart\n                    self.send_msg(chat_id, \"Перезапускаюсь…\", context=context)\n                    _restart.restart_self()\n                except Exception as e:\n                    logger.error(f\"Ошибка самоперезапуска: {e}\")\n                    self.send_msg(chat_id, \"Ошибка перезапуска.\", context=context)\n        elif data == \"restart_machine\":\n            if not self.get_admin_right(user_id, \"restart_machine\"):\n                query.edit_message_text(\"🚫 Доступ запрещён!\")\n                return\n            try:\n                self.dispatcher.bot.delete_message(chat_id, query.message.message_id)\n            except Exception as e:\n                logger.error(f\"Ошибка удаления меню: {e}\")\n            try:\n                subprocess.run(\n                    [\"sudo\", \"-S\", \"shutdown\", \"-r\", \"now\"],\n                    input=self.sudo_password + \"\\n\",\n                    text=True,\n                    capture_output=True,\n                    check=True\n                )\n                self.send_msg(chat_id, \"Машина перезагружается.\", context=context)\n            except Exception as e:\n                logger.error(f\"Ошибка при перезагрузке машины: {e}\")\n                self.send_msg(chat_id, \"Ошибка при перезагрузке машины.\", context=context)\n        elif data == \"shutdown_host\":\n            if not self.get_admin_right(user_id, \"shutdown_host\"):\n                self.send_msg(chat_id, \"🚫 Доступ запрещён!\", context=context)\n                return\n            try:\n                self.dispatcher.bot.delete_message(chat_id, query.message.message_id)\n            except Exception as e:\n                logger.error(f\"Ошибка удаления меню: {e}\")\n            self.shutdown_host(chat_id, context, user_id)\n        elif data == \"script_menu\":\n            self.show_script_menu(chat_id, context, user_id)\n        elif data.startswith(\"script_menu:\"):\n            script_name = data.split(\":\", 1)[1]\n            keyboard = [\n                [InlineKeyboardButton(\"Без параметров\", callback_data=f\"run_script_noparams:{script_name}\")],\n                [InlineKeyboardButton(\"С параметрами\", callback_data=f\"run_script_withparams:{script_name}\")],\n                [InlineKeyboardButton(\"🔙 Назад\", callback_data=\"script_menu\")]\n            ]\n            reply_markup = InlineKeyboardMarkup(keyboard)\n            query.edit_message_text(f\"Выберите режим выполнения скрипта {script_name}:\", reply_markup=reply_markup)\n        elif data.startswith(\"run_script_noparams:\"):\n            script_name = data.split(\":\", 1)[1]\n            self.run_script_common(chat_id, context, self._scripts_dir(), script_name, \"run_scripts\", params=\"\", user_id=user_id)\n        elif data.startswith(\"run_script_withparams:\"):\n            script_name = data.split(\":\", 1)[1]\n            context.chat_data[\"awaiting\"] = \"script_withparams\"\n            context.chat_data[\"script_name\"] = script_name\n            self.send_msg(chat_id, f\"Введите параметры для скрипта {script_name} (или оставьте поле пустым):\", context=context)\n        elif data == \"rights_menu\":\n            self.show_admin_selection_menu(chat_id, context, user_id)\n        elif data.startswith(\"select_admin_rights:\"):\n            target_id = data.split(\":\", 1)[1]\n            self.show_rights_menu_for_admin(chat_id, target_id, context, user_id)\n        elif data.startswith(\"notif_rights_menu:\"):\n            target_id = data.split(\":\", 1)[1]\n            self.show_notifications_rights_menu(chat_id, target_id, context, user_id)\n        elif data.startswith(\"toggle_return:\"):\n            target_id = data.split(\":\", 1)[1]\n            self.show_rights_menu_for_admin(chat_id, target_id, context, user_id)\n        elif data.startswith(\"toggle:\"):\n            # Формат toggle:<uid>:<right_key>; uid содержит ':' (\"telegram:123\"),\n            # имя права — никогда. Разбираем с конца.\n            parts_ = data[len(\"toggle:\"):].rsplit(\":\", 1)\n            if len(parts_) != 2 or not parts_[0] or not parts_[1]:\n                query.edit_message_text(\"Неверный формат команды.\")\n                return\n            target_id, right_key = parts_[0], parts_[1]\n            if not self.get_admin_right(user_id, \"manage_rights\"):\n                query.edit_message_text(\"🚫 Доступ запрещён!\")\n                return\n            new_status = self.toggle_admin_right(target_id, right_key)\n            # Если изменяется право с суффиксом _edit и новое значение True, устанавливаем соответствующее базовое право\n            if right_key.endswith(\"_edit\") and new_status:\n                base_key = right_key[:-5]\n                rights = load_admin_rights()\n                rights[_norm_key(target_id)][base_key] = True\n                save_admin_rights(rights)\n            # Обновляем список команд для пользователя, у которого изменились права\n            target_num = _numeric_uid(target_id)\n            if right_key in (\"ai_chat\", \"opencode_chat\", \"bot_status\") and target_num:\n                self.set_commands(user_id=target_num)\n            # Отправляем уведомление администратору о смене рабочего значения (для включения и выключения)\n            try:\n                if target_num:\n                    self.dispatcher.bot.send_message(\n                        chat_id=target_num,\n                        text=f\"Ваше право '{right_key}' было переключено на {'✅' if new_status else '❌'}.\"\n                    )\n            except Exception as e:\n                logger.error(f\"Ошибка отправки уведомления об изменении права '{right_key}' для администратора {target_id}: {e}\")\n            query.answer(text=f\"{right_key} переключено на {'✅' if new_status else '❌'} для ID {target_id}\", show_alert=True)\n            # Если переключается право уведомлений, остаёмся в подменю уведомлений, иначе возвращаемся к основному меню прав\n            if right_key.startswith(\"receive_notifications_\"):\n                self.show_notifications_rights_menu(chat_id, target_id, context, user_id)\n            else:\n                self.show_rights_menu_for_admin(chat_id, target_id, context, user_id)\n        # Обработка подменю уведомлений:\n        elif data == \"bot_notifications\":\n            self.show_notifications_menu(chat_id, context, user_id)\n        elif data == \"toggle_no_sound_notification\":\n            new_status = self.toggle_admin_right(user_id, \"receive_notifications_no_sound\")\n            self.dispatcher.bot.answer_callback_query(callback_query_id=query.id, text=f\"Отправлять без звука переключено на {'✅' if new_status else '❌'}\", show_alert=True)\n            self.show_notifications_menu(chat_id, context, user_id)\n        elif data == \"toggle_nmap_notification\":\n            new_status = self.toggle_admin_right(user_id, \"receive_notifications_nmap\")\n            self.dispatcher.bot.answer_callback_query(callback_query_id=query.id, text=f\"Уведомления Nmap переключены на {'✅' if new_status else '❌'}\", show_alert=True)\n            self.show_notifications_menu(chat_id, context, user_id)\n        elif data == \"toggle_guacamole_notification\":\n            new_status = self.toggle_admin_right(user_id, \"receive_notifications_guacamole\")\n            self.dispatcher.bot.answer_callback_query(callback_query_id=query.id, text=f\"Уведомления Guacamole переключены на {'✅' if new_status else '❌'}\", show_alert=True)\n            self.show_notifications_menu(chat_id, context, user_id)\n        elif data == \"toggle_f2b_notification\":\n            new_status = self.toggle_admin_right(user_id, \"receive_notifications_f2b\")\n            self.dispatcher.bot.answer_callback_query(callback_query_id=query.id, text=f\"Уведомления F2B переключены на {'✅' if new_status else '❌'}\", show_alert=True)\n            self.show_notifications_menu(chat_id, context, user_id)\n        elif data == \"toggle_wireguard_notification\":\n            new_status = self.toggle_admin_right(user_id, \"receive_notifications_wireguard\")\n            self.dispatcher.bot.answer_callback_query(callback_query_id=query.id, text=f\"Уведомления Wireguard переключены на {'✅' if new_status else '❌'}\", show_alert=True)\n            self.show_notifications_menu(chat_id, context, user_id)\n        elif data == \"toggle_status_notification\":\n            new_status = self.toggle_admin_right(user_id, \"receive_notifications_status\")\n            self.dispatcher.bot.answer_callback_query(callback_query_id=query.id, text=f\"Уведомления Status переключены на {'✅' if new_status else '❌'}\", show_alert=True)\n            self.show_notifications_menu(chat_id, context, user_id)\n        elif data == \"toggle_new_user_notification\":\n            new_status = self.toggle_admin_right(user_id, \"receive_notifications_new_user\")\n            self.dispatcher.bot.answer_callback_query(callback_query_id=query.id, text=f\"Уведомления о новых пользователях переключены на {'✅' if new_status else '❌'}\", show_alert=True)\n            self.show_notifications_menu(chat_id, context, user_id)\n        elif data == \"toggle_zabbix_notification\":\n            new_status = self.toggle_admin_right(user_id, \"receive_notifications_zabbix\")\n            self.dispatcher.bot.answer_callback_query(callback_query_id=query.id, text=f\"Уведомления Zabbix переключены на {'✅' if new_status else '❌'}\", show_alert=True)\n            self.show_notifications_menu(chat_id, context, user_id)\n        elif data == \"get_latest_messages\":\n            self.show_latest_messages(chat_id, context, user_id)\n        # service_* обслуживает модуль services (ui_tg, группа -1).\n\n        elif data == \"noop\":\n            query.answer()\n        elif data == \"my_id\":\n            query.edit_message_text(f\"Ваш ID: {user_id}\")\n        elif data == \"console_session\":\n            self.start_console_session(chat_id, context, user_id)\n        else:\n            query.edit_message_text(\"Неизвестная команда.\")\n\n    def show_admin_selection_menu(self, chat_id, context, user_id):\n        if not self.get_admin_right(user_id, \"manage_rights\"):\n            self.send_msg(chat_id, \"🚫 Доступ запрещён!\", context=context)\n            return\n        rights = load_admin_rights()\n        admin_ids = set(_norm_key(x) for x in _admin_ids()) | set(rights.keys())\n        buttons = []\n        for aid in sorted(admin_ids):\n            buttons.append(InlineKeyboardButton(f\"👤 ID: {aid}\", callback_data=f\"select_admin_rights:{aid}\"))\n        rows = [buttons[i:i+2] for i in range(0, len(buttons), 2)]\n        rows.append([InlineKeyboardButton(\"🔙 Назад\", callback_data=\"main_menu\")])\n        reply_markup = InlineKeyboardMarkup(rows)\n        self.send_msg(chat_id, \"🛠 Выберите администратора для управления правами:\", reply_markup=reply_markup, context=context)\n\n    def show_rights_menu_for_admin(self, chat_id, target_id, context, user_id):\n        if not self.get_admin_right(user_id, \"manage_rights\"):\n            self.send_msg(chat_id, \"🚫 Доступ запрещён!\", context=context)\n            return\n        rights = load_admin_rights()\n        uid = _norm_key(target_id)\n        if uid not in rights:\n            rights[uid] = _access().guest_defaults()\n            save_admin_rights(rights)\n        # Подтянем новые ключи из реестра ядра (динамика)\n        changed = False\n        for k, v in _access().guest_defaults().items():\n            if k not in rights[uid]:\n                rights[uid][k] = v\n                changed = True\n        if changed:\n            save_admin_rights(rights)\n        user_rights = rights[uid]\n        text_lines = [f\"🛠 Права для ID: {uid}\"]\n        # Разбиваем ключи на общие и уведомлений (те, что начинаются с \"receive_notifications_\")\n        general_keys = [key for key in user_rights.keys() if not key.startswith(\"receive_notifications_\") and not key.startswith(\"pve_manage_hosts\")]\n        notif_keys = [key for key in user_rights.keys() if key.startswith(\"receive_notifications_\")]\n\n        buttons = []\n        # Формируем кнопки для общих прав\n        for key in general_keys:\n            allowed = user_rights.get(key, False)\n            emoji = \"✅\" if allowed else \"❌\"\n            buttons.append(InlineKeyboardButton(f\"{key.capitalize()} {emoji}\", callback_data=f\"toggle:{uid}:{key}\"))\n        # Добавляем отдельную кнопку для перехода в подменю прав уведомлений\n        if notif_keys:\n            buttons.append(InlineKeyboardButton(\"🔔 Права уведомлений\", callback_data=f\"notif_rights_menu:{uid}\"))\n        # Добавляем кнопку для прав PVE\n        buttons.append(InlineKeyboardButton(\"🏠 Права PVE\", callback_data=f\"pve_rights_menu:{uid}\"))\n\n        rows = [buttons[i:i+2] for i in range(0, len(buttons), 2)]\n        rows.append([InlineKeyboardButton(\"🔙 Назад\", callback_data=\"rights_menu\"),\n                       InlineKeyboardButton(\"🏠 Главное меню\", callback_data=\"main_menu\")])\n        reply_markup = InlineKeyboardMarkup(rows)\n        text = \"\\n\".join(text_lines)\n        self.send_msg(chat_id, text, reply_markup=reply_markup, context=context)\n\n    def show_notifications_rights_menu(self, chat_id, target_id, context, user_id):\n        rights = load_admin_rights()\n        uid = _norm_key(target_id)\n        if uid not in rights:\n            rights[uid] = _access().guest_defaults()\n            save_admin_rights(rights)\n        # Подтянем новые ключи из реестра ядра (динамика)\n        changed = False\n        for k, v in _access().guest_defaults().items():\n            if k not in rights[uid]:\n                rights[uid][k] = v\n                changed = True\n        if changed:\n            save_admin_rights(rights)\n        user_rights = rights[uid]\n        text_lines = [f\"🔔 Права уведомлений для ID: {uid}:\"]\n        # Выбираем только ключи, начинающиеся с \"receive_notifications_\"\n        notif_keys = [key for key in user_rights.keys() if key.startswith(\"receive_notifications_\")]\n        buttons = []\n        for key in notif_keys:\n            allowed = user_rights.get(key, False)\n            emoji = \"✅\" if allowed else \"❌\"\n            # Для наглядности убираем префикс в названии кнопки\n            display_name = key.replace(\"receive_notifications_\", \"\").capitalize()\n            buttons.append(InlineKeyboardButton(f\"{display_name} {emoji}\", callback_data=f\"toggle:{uid}:{key}\"))\n        rows = [buttons[i:i+2] for i in range(0, len(buttons), 2)]\n        # Кнопка возврата в основное меню прав\n        rows.append([InlineKeyboardButton(\"🔙 Назад\", callback_data=f\"toggle_return:{uid}\")])\n        reply_markup = InlineKeyboardMarkup(rows)\n        text = \"\\n\".join(text_lines)\n        self.send_msg(chat_id, text, reply_markup=reply_markup, context=context)\n\n    def handle_text(self, update, context):\n        chat_id = update.message.chat.id\n        message_id = update.message.message_id\n        user = update.effective_user\n        user_id = user.id\n        text = update.message.text or \"\"\n        # Режим OpenCode (модуль opencode_chat, группа -2) имеет приоритет:\n        # если у пользователя активна сессия opencode — пропускаем обработку здесь.\n        if self._opencode_active(user_id):\n            raise DispatcherHandlerStop()\n        logger.info(\"AdminMenu: User %s (ID: %s) отправил текст в меню: %s\", user.username, user_id, text)\n        # для групп сохраняем с chat_id\n        try:\n            context._group_log_chat = {\"id\": update.effective_chat.id, \"type\": update.effective_chat.type, \"title\": getattr(update.effective_chat, 'title', '')}\n        except Exception:\n            pass\n        self.log_user_message(user, update.message.text, context)\n\n        state = context.chat_data.get(\"awaiting\")\n        if state:\n            context.chat_data.pop(\"awaiting\", None)\n            if state == \"add_admin\":\n                self.process_add_admin(update, context, update.message.text)\n            elif state == \"remove_admin\":\n                self.process_remove_admin(update, context, update.message.text)\n            elif state == \"script_withparams\":\n                script_name = context.chat_data.pop(\"script_name\", None)\n                self.run_script_common(chat_id, context, self._scripts_dir(), script_name, \"run_scripts\", params=update.message.text, user_id=user_id)\n            else:\n                self.send_msg(chat_id, \"Неожиданный ввод.\", context=context)\n            \n            try:\n                context.bot.delete_message(chat_id, message_id)\n            except Exception as e:\n                logger.error(f\"Ошибка удаления сообщения: {e}\")\n            return\n        \n        if user_id in self.console_sessions:\n            self.process_console_command(chat_id, context, user_id, update.message.text)\n            try:\n                context.bot.delete_message(chat_id, message_id)\n            except Exception as e:\n                logger.error(f\"Ошибка удаления сообщения: {e}\")\n            return\n        \n        # В группах не спамим \"Нераспознанная команда\" — мешает групповому чату (реплаи/! триггеры)\n        if update.effective_chat.type != \"private\":\n            return\n        # Если ни одно из условий не выполнено, выводим сообщение об ошибке.\n        self.send_msg(chat_id, \"Нераспознанная команда. Используйте меню через /start.\", context=context)\n        try:\n            context.bot.delete_message(chat_id, message_id)\n        except Exception as e:\n            logger.error(f\"Ошибка удаления сообщения: {e}\")\n\n    def _opencode_active(self, user_id):\n        \"\"\"True, если у пользователя активна сессия opencode_chat.\"\"\"\n        refs = getattr(self, \"_module_refs\", None)\n        if not isinstance(refs, dict):\n            return False\n        oc = refs.get(\"opencode_chat\")\n        return bool(oc and getattr(oc, \"is_active\", None) and oc.is_active(user_id))\n\n    def error_handler(self, update, context):\n        if update and update.message:\n            user = update.message.from_user\n            logger.error(f\"Ошибка от {user.username} ({user.id}): {context.error}\")\n\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> subprocess</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> time</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> select</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> copy</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> datetime </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> datetime, timedelta</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> telegram </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> BotCommand, BotCommandScopeChat, InlineKeyboardMarkup, InlineKeyboardButton</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> telegram.ext </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> CommandHandler, MessageHandler, Filters, CallbackQueryHandler, DispatcherHandlerStop</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> telegram.utils.helpers </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> escape_markdown</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging.handlers </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> RotatingFileHandler</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pexpect</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">logger </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging.getLogger(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__name__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Небольшой кэш прав администраторов (TTL) – снижает количество чтений файла с диска</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># при отрисовке меню, где get_admin_right вызывается десятки раз подряд.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Лок защищает файл от гонок: права теперь трогают и другие потоки (web-канал).</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">_rights_lock </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading.RLock()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">_rights_cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"data\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0.0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_RIGHTS_TTL</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 5.0</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Пространство имён мессенджера для ключей прав. Telegram- и VK-пользователи</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># имеют независимые ID, поэтому ключи вида \"telegram:&#x3C;id>\" / \"vk:&#x3C;id>\".</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_RIGHTS_NS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"telegram\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _access</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">():</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Модуль access (единый владелец ADMIN_IDS/прав). Лениво — без порядка загрузки.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> botmod_access </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> engine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _acc</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _acc</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _admin_ids</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _access().admin_ids()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _norm_key</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(uid):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Квалифицированный ключ прав. Идемпотентен: уже квалифицированный</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    (\"telegram:123\") возвращает как есть, сырой числовой id оборачивает.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    uid_s </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(uid)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \":\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> uid_s:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> uid_s</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{_RIGHTS_NS}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">uid_s</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _numeric_uid</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(uid):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Числовой id из квалифицированного ключа прав ('telegram:123' -> 123).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Возвращает None, если цифр в конце нет.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re.search(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">(\\d</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">)$</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(uid))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(m.group(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> load_admin_rights</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">():</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"BOTMOD: живьём из access (без TTL-кеша и прямых чтений файла).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _access().load_rights()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> save_admin_rights</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(rights):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"BOTMOD: дифф-запись через ядро (атомарно).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        _access().save_rights(rights </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {})</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка сохранения прав администраторов: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> delete_message</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(context):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    job </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> context.job</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        context.bot.delete_message(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">chat_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">job.context[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">message_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">job.context[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"message_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Message to delete not found\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.debug(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Сообщение для удаления не найдено, пропускаем.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка при удалении сообщения: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> flush_buffer</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(child):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Читает доступные данные из буфера, чтобы очистить его перед выполнением новой команды.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    flushed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        while</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            flushed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> child.read_nonblocking(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">size</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1024</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">timeout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (pexpect.exceptions.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TIMEOUT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, pexpect.exceptions.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">EOF</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        pass</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> flushed</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">_console_handler </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> log_console</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(user_id, text):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Записывает команды/вывод консольных сессий в отдельный файл (config.LOG_CONSOLE).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Файл ротируется по размеру, суммарный объём ограничен config.LOG_CONSOLE_MAX_MB.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    global</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _console_handler</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        log_path </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">LOG_CONSOLE</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        os.makedirs(os.path.dirname(log_path), </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">exist_ok</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _console_handler </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _console_handler.baseFilename </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.abspath(log_path):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            backup_count </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            max_bytes </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> max</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1024</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">LOG_CONSOLE_MAX_MB</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> *</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1024</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> *</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1024</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">//</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (backup_count </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            _console_handler </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> RotatingFileHandler(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                log_path, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">maxBytes</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">max_bytes, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">backupCount</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">backup_count, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            )</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            _console_handler.setFormatter(logging.Formatter(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%(asctime)s</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> %(message)s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        timestamp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> datetime.now().strftime(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'%Y-%m-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> %H:%M:%S'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        _console_handler.emit(logging.LogRecord(</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            name</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"console_sessions\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            level</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">logging.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">INFO</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            pathname</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__file__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            lineno</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            msg</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">timestamp</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">] [user:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user_id</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">] </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">text</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            args</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            exc_info</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        _console_handler.flush()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка записи лога консоли: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> format_online</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(seconds):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Форматирует секунды в человекочитаемый вид: «3д 04:12» / «1:05:33» / «00:42».\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    seconds </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(seconds </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    d, rem </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> divmod</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(seconds, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">86400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    h, rem </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> divmod</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(rem, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">3600</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    m, s </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> divmod</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(rem, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">60</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">д </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">h</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:02d</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:02d</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> h:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">h</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:02d</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:02d</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">s</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:02d</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:02d</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">s</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:02d</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># --- Функционал кеширования сообщений и действий пользователей ---</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> load_user_message_cache</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">():</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: [], </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"users\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: []}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.exists(config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">CACHE_USER_MESSAGES</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">CACHE_USER_MESSAGES</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"r\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json.load(f)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"messages\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            cache[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"users\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            cache[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"users\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Удаляем сообщения старше 30 дней</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        threshold </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> datetime.now() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> timedelta(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">days</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">30</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        cache[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> datetime.fromisoformat(msg[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"time\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">>=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threshold]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка загрузки кеша сообщений: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> save_user_message_cache</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(cache):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">CACHE_USER_MESSAGES</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"w\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            json.dump(cache, f, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">indent</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">4</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка сохранения кеша сообщений: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># --- Конец блока кеширования ---</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">class</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> AdminMenu</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> __init__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, dispatcher):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dispatcher</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # BOTMOD: список администраторов — только через access (_admin_ids()).</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.sudo_password </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ADMIN_PASS</span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">  # Пароль sudo из переменной окружения ADMIN_PASS</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.check_and_update_admin_rights()</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Словарь для хранения последнего отправленного ботом сообщения по каждому чату</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.last_bot_message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Словарь активных консольных сессий: ключ – user_id, значение – объект (pexpect или subprocess)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.console_sessions </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    </span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # --- Логирование сообщений и действий пользователей ---</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> log_user_message</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, user, text, context):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> load_user_message_cache()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        new_user </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user.id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"users\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_user:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            cache[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"users\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].append(user.id)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.notify_new_user(user, context)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # определяем чат для группы/лички</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chat </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            chat </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> context.bot.get_me  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># dummy to keep context ref</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # эффективный чат из context</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> hasattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(context, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'chat_data'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                pass</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # пробуем взять из update через context? fallback</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chat_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chat_type </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"private\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chat_title </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # context may have effective_chat in some calls, but here we use passed update? caller passes user+text+context, we need update</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # попробуем достать из context.chat_data? не надёжно, поэтому требуем чтобы вызывающий передал chat</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        message_entry </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"user_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: user.id,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: user.username </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user.username </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user.first_name,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"text\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: text,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"time\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: datetime.now().isoformat(),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"chat_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"chat_type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"private\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"chat_title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"is_group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # попытаемся извлечь чат из контекста вызывающего — если в context есть _group_log_chat</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            gc </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(context, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'_group_log_chat'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> gc:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message_entry[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(gc.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> gc.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message_entry[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat_type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> gc.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"private\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message_entry[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat_title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> gc.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message_entry[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"is_group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> message_entry[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat_type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"supergroup\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        cache[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].append(message_entry)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        save_user_message_cache(cache)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> log_user_message_with_chat</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, user, text, chat):</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # прямой вызов с объектом чата (для групп)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> load_user_message_cache()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user.id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"users\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            cache[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"users\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].append(user.id)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        entry </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"user_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: user.id,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: user.username </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user.username </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user.first_name,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"text\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: text,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"time\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: datetime.now().isoformat(),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"chat_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(chat, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'id'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">''</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> ''</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"chat_type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(chat, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'type'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'private'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> 'private'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"chat_title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(chat, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'title'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">''</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> ''</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"is_group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(chat, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'type'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">''</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'group'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'supergroup'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        cache[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].append(entry)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        save_user_message_cache(cache)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> notify_new_user</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, user, context):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Отправляет уведомление о новом пользователе администраторам, у которых включены</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        и общее право receive_notifications, и подчинённое receive_notifications_new_user.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Если включено receive_notifications_no_sound – отправляет без звука.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> admin_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _admin_ids():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(admin_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> </span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(admin_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_new_user\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                no_sound </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(admin_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_no_sound\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.send_message(</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                        chat_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">admin_id,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                        text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Новый пользователь: ID: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user.id</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">, Имя: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user.username </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user.username </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user.first_name</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                        disable_notification</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">no_sound</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    )</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка отправки уведомления администратору </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">admin_id</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # --- Конец логирования ---</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> check_and_update_admin_rights</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # BOTMOD: сидинг/чистка — access.seed(); pve_manage_hosts дотягивает pve-модуль.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            _access().seed()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка синхронизации прав: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> get_admin_right</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, user_id, key):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _access().get_right(user_id, key)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> toggle_admin_right</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, target_id, key):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _access().toggle_right(_norm_key(target_id), key)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка переключения права: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # Обработка уведомлений по добавлению/удалению администраторов.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> process_add_admin</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, update, context, admin_id_text):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chat_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.message.chat.id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.callback_query.message.chat.id</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_user</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user.id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manage_admins\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        new_admin </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> admin_id_text.strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_admin.isdigit():</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Неверный формат ID.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # BOTMOD: скрипт правил старый config.py — теперь access (.env + seed).</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            changed, output </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _access().add_admin_id(new_admin)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Результат добавления администратора:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">output</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            notify_msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Внимание!</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Администратор </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">new_admin</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> добавлен пользователем \"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user.username</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user.id</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">).</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Для вступления изменений перезапустите бота.\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            )</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> admin </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _admin_ids():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> admin </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user.id:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.send_message(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">chat_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">admin, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">notify_msg)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка при добавлении администратора: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка при добавлении администратора.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> process_remove_admin</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, update, context, admin_id_text):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chat_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.message.chat.id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.callback_query.message.chat.id</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_user</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user.id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manage_admins\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        rem_admin </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> admin_id_text.strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rem_admin.isdigit():</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Неверный формат ID.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # BOTMOD: см. выше — access вместо скрипта.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            cfg_changed, _rights_changed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _access().remove_admin_id(rem_admin)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            output </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Администратор </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">rem_admin</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> удалён.\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                      +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\" (требуется перезапуск)\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cfg_changed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Результат удаления администратора:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">output</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            notify_msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Внимание!</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Администратор </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">rem_admin</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> удалён пользователем \"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user.username</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user.id</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">).</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Для вступления изменений перезапустите бота.\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            )</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> admin </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _admin_ids():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> admin </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user.id:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.send_message(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">chat_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">admin, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">notify_msg)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка при удалении администратора: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка при удалении администратора.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # --- Xiaomi miIO ---</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> send_msg</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, chat_id, text, reply_markup</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, auto_delete</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, parse_mode</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> chat_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.last_bot_message:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.delete_message(chat_id, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.last_bot_message[chat_id])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Message to delete not found\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    logger.debug(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Сообщение для удаления не найдено, пропускаем.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка при удалении сообщения: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.last_bot_message.pop(chat_id, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.send_message(</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            chat_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">chat_id, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">text, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">reply_markup</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">reply_markup, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">parse_mode</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">parse_mode</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        )</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.last_bot_message[chat_id] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg.message_id</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> auto_delete </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> context </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.schedule_deletion(context, chat_id, msg.message_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> schedule_deletion</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, context, chat_id, message_id, delay</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">60</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            context.job_queue.run_once(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                delete_message, delay, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"message_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: message_id}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            )</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка планирования удаления: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> register_handlers</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.add_handler(CommandHandler(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"start\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.start))</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.add_handler(CommandHandler(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_my_id))</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.add_handler(CallbackQueryHandler(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.button))</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.add_handler(MessageHandler(Filters.text </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ~</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Filters.command, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.handle_text))</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.set_commands()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> set_commands</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, user_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Устанавливает команды бота.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Глобально (user_id is None) — только базовые /start и /id, чтобы</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        пользователи без прав не видели привилегированные команды.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Для конкретного user_id — персональный набор через BotCommandScopeChat</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        с учётом прав (ai_chat, opencode_chat, bot_status и т.д.).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # BOTMOD: глобальный список команд публикует transport_telegram</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # (динамически из TG_COMMANDS модулей) — движок его не затирает.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Персональный scope по правам — как в старом боте.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            commands </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                BotCommand(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"start\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Запуск меню\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                BotCommand(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Получить свой ID\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                BotCommand(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"groupid\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Узнать ID группы\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ai_chat\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                commands.append(BotCommand(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ai\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Задать вопрос ИИ (без контекста)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                commands.append(BotCommand(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"aiclose\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Завершить разговор с ИИ\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                commands.append(BotCommand(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"aimodel\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Выбор модели AI-чата\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                commands.append(BotCommand(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"aithink\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Переключить thinking\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"opencode_chat\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                commands.append(BotCommand(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"oc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Задать вопрос OpenCode (MCP)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                commands.append(BotCommand(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ocreset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Завершить разговор с OpenCode\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                commands.append(BotCommand(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"model\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Выбор модели OpenCode\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                commands.append(BotCommand(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"octhink\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Переключить thinking OC\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"schedule_manager\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                commands.append(BotCommand(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ocsched\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Отложенные задачи\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"bot_status\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                commands.append(BotCommand(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"status\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Статус бота\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"group_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                commands.append(BotCommand(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"groupadd\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Добавить группу\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                commands.append(BotCommand(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"groupdel\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Удалить группу\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                commands.append(BotCommand(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"grouptoggle\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Вкл/выкл группу\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # groupid уже публично выше</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.set_my_commands(commands, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">scope</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">BotCommandScopeChat(user_id))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка при установке команд: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> is_admin</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, user_id):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _access().is_admin(user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> show_main_menu</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, chat_id, user_id, context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            provider </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"tgmenu_provider\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            spec </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> provider(user_id) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> callable</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(provider) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            spec </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        keyboard </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> spec </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Динамическое меню: кнопки загруженных модулей (порядок/ширина/</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # hidden — data/tg_menu.json, права уже отфильтрованы провайдером).</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Ширина — доли строки, ряд добирается жадно (auto = половина).</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            slots </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0.25</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"2/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0.5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"3/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0.75</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                     \"1/3\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1.0</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> /</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 3.0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"2/3\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2.0</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> /</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 3.0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            row, fill </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [], </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0.0</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> spec:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                btn </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardButton(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    b.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">b.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"callback\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                w </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"width\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"auto\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> w </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"full\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> row:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        keyboard.append(row)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        row, fill </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [], </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0.0</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    keyboard.append([btn])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                need </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> slots.get(w, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0.5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> row </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fill </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> need </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1.0</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1e-9</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    keyboard.append(row)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    row, fill </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [], </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0.0</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                row.append(btn)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                fill </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> need</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> row:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                keyboard.append(row)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        reply_markup </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardMarkup(keyboard)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Главное меню:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">reply_markup</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">reply_markup, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> start</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, update, context):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.message:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                context.bot.delete_message(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">chat_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">update.message.chat.id,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                                           message_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">update.message.message_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка удаления команды /start: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.callback_query.message</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_user</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user.id</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.set_commands(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">user_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.is_admin(user_id):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            keyboard </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [[InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Получить мой ID\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"my_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            reply_markup </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardMarkup(keyboard)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(msg.chat.id,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                          \"Доступ к основному меню ограничен.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Но вы можете узнать свой ID:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                          reply_markup</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">reply_markup, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_main_menu(msg.chat.id, user_id, context)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> get_my_id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, update, context):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.message:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                context.bot.delete_message(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">chat_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">update.message.chat.id,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                                           message_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">update.message.message_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка удаления команды /id: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_user</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            update.effective_chat.id,</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ваш ID: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user.id</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            auto_delete</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        )</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> shutdown_host</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, chat_id, context, user_id):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"shutdown_host\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            subprocess.run(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"sudo\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-S\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"shutdown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-h\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"now\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                input</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.sudo_password </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                capture_output</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                check</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            )</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Хост выключается.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка при выключении хоста: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка при выключении хоста.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> start_console_session</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, chat_id, context, user_id):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"console_session\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.console_sessions:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Консоль уже запущена. Для завершения введите #quitconsole.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            child </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pexpect.spawn(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/bin/bash\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">echo</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">timeout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            child.sendline(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"export PS1='[PEXPECT]>'\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            child.expect_exact(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"[PEXPECT]>\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.console_sessions[user_id] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> child</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            log_console(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"СЕССИЯ ЗАПУЩЕНА\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                chat_id,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"Консоль запущена. Введите команды. Для завершения сессии введите #quitconsole.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            )</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка при запуске консоли: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка при запуске консоли.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> process_console_command</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, chat_id, context, user_id, command):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        child </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.console_sessions.get(user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> child:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Консольная сессия не активна.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> command.strip() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"#quitconsole\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                child.sendline(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"exit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                child.expect(pexpect.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">EOF</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                pass</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            del</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.console_sessions[user_id]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            log_console(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"СЕССИЯ ЗАВЕРШЕНА\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Консоль завершена.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            flush_buffer(child)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            marker </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"__END_OF_CMD__\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            composite_command </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">command</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">; echo </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">marker</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            child.sendline(composite_command)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            output </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            while</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                index </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> child.expect([marker, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">Are you sure you want to continue connecting</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, pexpect.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">EOF</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, pexpect.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TIMEOUT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">timeout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">10</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> index </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    output </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> child.before</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    break</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> index </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    output </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> child.before</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    child.sendline(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"yes\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> index </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    output </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> child.before</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    break</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> output.startswith(command):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                output </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> output[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(command):].strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> output:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                output </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Команда выполнена, без вывода.\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            log_console(user_id, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"$ </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">command</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">output</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Вывод команды:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">output</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка выполнения команды в консоли: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка выполнения команды в консоли.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> show_admin_menu</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, chat_id, context, user_id):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manage_admins\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        keyboard </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        row </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        row.append(InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"➕ Добавить\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"add_admin\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        row.append(InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"❌ Удалить\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"remove_admin\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        keyboard.append(row)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        keyboard.append([InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔙 Назад\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"main_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        reply_markup </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardMarkup(keyboard)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"👮‍♂️ Меню администрирования:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">reply_markup</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">reply_markup, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> show_bot_menu</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, chat_id, user_id, context):</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Проверяем наличие прав для управления ботом</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart_bot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> </span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart_machine\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> </span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"shutdown_host\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> </span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"view_user_messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        keyboard </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        row </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart_bot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            row.append(InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🤖 Перезапуск бота\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart_bot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart_machine\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            row.append(InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔄 Перезагрузка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart_machine\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> row:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            keyboard.append(row)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        row </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"shutdown_host\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            row.append(InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"⏻ Выключить хост\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"shutdown_host\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"view_user_messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            row.append(InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📩 Получить последние сообщения\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"get_latest_messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> row:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            keyboard.append(row)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        keyboard.append([InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔙 Назад\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"main_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        reply_markup </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardMarkup(keyboard)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🤖 Меню управления ботом:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">reply_markup</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">reply_markup, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    </span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _scripts_dir</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Каталог пользовательских скриптов: &#x3C;cwd>/scripts, fallback —</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        каталог скриптов модуля services (BOTMOD).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(os.getcwd(), </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isdir(d):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                os.path.dirname(os.path.abspath(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__file__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"..\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"services\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> show_script_menu</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, chat_id, context, user_id):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"run_scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        scripts_dir </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._scripts_dir()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            files </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.listdir(scripts_dir)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка доступа к каталогу скриптов.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        script_files </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [f </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> files </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f.endswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".sh\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> script_files:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Нет доступных скриптов.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        keyboard </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        row </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> script </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(script_files):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            row.append(InlineKeyboardButton(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📄 </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">script</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"script_menu:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">script</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(row) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                keyboard.append(row)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                row </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> row:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            keyboard.append(row)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        keyboard.append([InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔙 Назад\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"main_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        reply_markup </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardMarkup(keyboard)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📜 Выберите скрипт для выполнения:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">reply_markup</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">reply_markup, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> run_script_common</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, chat_id, context, folder, script_name, required_right, params</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, user_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, required_right):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # BOTMOD: скрипты — вендор пакета; относительный folder резолвится</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # от директории пакета, а не от CWD процесса.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.isabs(folder):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            script_path </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(folder, script_name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            script_path </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                os.path.dirname(os.path.abspath(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__file__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)), folder, script_name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.exists(script_path):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"❌ Скрипт </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">script_name</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> не найден.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        cmd_list </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"sudo\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-S\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"bash\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, script_path]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> params:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            dangerous_pattern </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re.compile(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">[;&#x26;|`$()]</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            param_list </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> params.split()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> param </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> param_list:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dangerous_pattern.search(param):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Недопустимый параметр: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">param</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            cmd_list.extend(param_list)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Не поднимаем исключение при ненулевом коде возврата — проверяем вручную</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            result </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> subprocess.run(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                cmd_list,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                input</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.sudo_password </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                capture_output</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            )</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> result.returncode </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                raw_help_text </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> result.stdout.strip() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> result.stderr.strip() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Скрипт </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">script_name</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> завершился с ошибкой.\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                help_text </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> escape_markdown(raw_help_text, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">version</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    chat_id,</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"⚠️ Ошибка выполнения скрипта </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">escape_markdown(script_name, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">version</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">```</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">help_text</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">```\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                    context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                    parse_mode</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MarkdownV2\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                )</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                output </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> result.stdout.strip() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Скрипт </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">script_name</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> выполнен без вывода.\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                output_escaped </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> escape_markdown(output, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">version</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    chat_id,</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📄 Вывод скрипта </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">escape_markdown(script_name, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">version</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">```</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">output_escaped</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">```\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                    context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                    parse_mode</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MarkdownV2\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                )</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка run_script_common для </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">script_name</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            err_text </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> escape_markdown(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e), </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">version</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                chat_id,</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"⚠️ Ошибка при выполнении скрипта </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">escape_markdown(script_name, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">version</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">```</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">err_text</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">```\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                parse_mode</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MarkdownV2\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            )</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> show_notifications_menu</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, chat_id, context, user_id):</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Доступ к меню уведомлений только при наличии общего права receive_notifications</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"У вас нет общего права receive_notifications, поэтому меню уведомлений недоступно.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Кнопка для переключения отправки уведомлений без звука</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        no_sound_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"✅\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_no_sound\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"❌\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        keyboard </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [[InlineKeyboardButton(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Отправлять без звука </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">no_sound_status</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toggle_no_sound_notification\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]]</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Добавляем кнопки для подчинённых уведомлений, если у администратора есть соответствующие _edit права</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        subordinate_buttons </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_nmap_edit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            stat </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"✅\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_nmap\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"❌\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            subordinate_buttons.append(InlineKeyboardButton(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Nmap </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">stat</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toggle_nmap_notification\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_guacamole_edit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            stat </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"✅\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_guacamole\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"❌\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            subordinate_buttons.append(InlineKeyboardButton(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Guacamole </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">stat</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toggle_guacamole_notification\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_f2b_edit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            stat </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"✅\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_f2b\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"❌\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            subordinate_buttons.append(InlineKeyboardButton(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Fail2Ban </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">stat</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toggle_f2b_notification\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_wireguard_edit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            stat </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"✅\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_wireguard\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"❌\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            subordinate_buttons.append(InlineKeyboardButton(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Wireguard </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">stat</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toggle_wireguard_notification\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_status_edit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            stat </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"✅\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_status\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"❌\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            subordinate_buttons.append(InlineKeyboardButton(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Status </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">stat</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toggle_status_notification\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_new_user_edit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            stat </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"✅\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_new_user\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"❌\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            subordinate_buttons.append(InlineKeyboardButton(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Новые пользователи </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">stat</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toggle_new_user_notification\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_zabbix_edit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            stat </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"✅\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_zabbix\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"❌\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            subordinate_buttons.append(InlineKeyboardButton(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Zabbix </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">stat</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toggle_zabbix_notification\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Разбиваем кнопки по 2 в строке</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> subordinate_buttons:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            rows </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [subordinate_buttons[i:i</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> i </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> range</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(subordinate_buttons), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            keyboard.extend(rows)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            keyboard.append([InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Нет дополнительных уведомлений\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"noop\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        keyboard.append([InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔙 Назад\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"main_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        reply_markup </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardMarkup(keyboard)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Меню уведомлений бота:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">reply_markup</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">reply_markup, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> show_latest_messages</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, chat_id, context, user_id):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"view_user_messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> load_user_message_cache()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        messages </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, []):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            dt_str </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"time\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">][:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">19</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            messages.append(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">dt_str</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> - ID: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">msg[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'user_id'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> - </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">msg[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'name'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">msg[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'text'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        result </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(messages)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        while</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> result </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(result) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 4096</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> messages:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            messages.pop(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            result </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(messages)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> result:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            result </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Нет сообщений в кеше.\"</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, result, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, update, context):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        query </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.callback_query</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> query.from_user</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user.id</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.log_user_message(user, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"[Callback] </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">query.data</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, context)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        logger.info(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"AdminMenu: User </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (ID: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) нажал кнопку с данными: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, user.username, user_id, query.data)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.is_admin(user_id) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> query.data.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"my_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            query.answer()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            query.edit_message_text(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        query.answer()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> query.data</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chat_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> query.message.chat.id</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"main_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_main_menu(chat_id, user_id, context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"admin_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_admin_menu(chat_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"add_admin\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            context.chat_data[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"awaiting\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"add_admin\"</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Введите ID нового администратора:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"remove_admin\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            context.chat_data[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"awaiting\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"remove_admin\"</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Введите ID администратора для удаления:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"bot_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_bot_menu(chat_id, user_id, context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"restart_bot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart_bot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                query.edit_message_text(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Не редактируем уже удалённое сообщение – удаляем меню и отправляем результат новой строкой</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.delete_message(chat_id, query.message.message_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка удаления меню: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            unit </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(config, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SYSTEMD_SERVICE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> unit:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                # A: рестарт через systemd (полный, как раньше; юнит из конфига).</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    subprocess.run(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"sudo\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-S\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"systemctl\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, unit],</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                        input</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.sudo_password </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                        text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                        capture_output</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                        check</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    )</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Бот перезапущен (systemd).\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка при перезапуске бота: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка перезапуска.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                # B: самоперезапуск execv (без sudo/systemd; сначала ответ).</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> restart </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _restart</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Перезапускаюсь…\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    _restart.restart_self()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка самоперезапуска: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка перезапуска.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"restart_machine\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart_machine\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                query.edit_message_text(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.delete_message(chat_id, query.message.message_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка удаления меню: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                subprocess.run(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"sudo\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-S\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"shutdown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-r\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"now\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                    input</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.sudo_password </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                    text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                    capture_output</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                    check</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                )</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Машина перезагружается.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка при перезагрузке машины: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка при перезагрузке машины.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"shutdown_host\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"shutdown_host\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.delete_message(chat_id, query.message.message_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка удаления меню: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.shutdown_host(chat_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"script_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_script_menu(chat_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"script_menu:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            script_name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\":\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            keyboard </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                [InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Без параметров\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"run_script_noparams:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">script_name</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                [InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"С параметрами\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"run_script_withparams:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">script_name</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                [InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔙 Назад\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"script_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            reply_markup </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardMarkup(keyboard)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            query.edit_message_text(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Выберите режим выполнения скрипта </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">script_name</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">reply_markup</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">reply_markup)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"run_script_noparams:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            script_name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\":\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.run_script_common(chat_id, context, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._scripts_dir(), script_name, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"run_scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">params</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">user_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"run_script_withparams:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            script_name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\":\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            context.chat_data[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"awaiting\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"script_withparams\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            context.chat_data[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"script_name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> script_name</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Введите параметры для скрипта </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">script_name</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (или оставьте поле пустым):\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"rights_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_admin_selection_menu(chat_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"select_admin_rights:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            target_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\":\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_rights_menu_for_admin(chat_id, target_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"notif_rights_menu:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            target_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\":\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_notifications_rights_menu(chat_id, target_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toggle_return:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            target_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\":\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_rights_menu_for_admin(chat_id, target_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toggle:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Формат toggle:&#x3C;uid>:&#x3C;right_key>; uid содержит ':' (\"telegram:123\"),</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # имя права — никогда. Разбираем с конца.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            parts_ </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toggle:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):].rsplit(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\":\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(parts_) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 2</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> or</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> parts_[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> parts_[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                query.edit_message_text(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Неверный формат команды.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            target_id, right_key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> parts_[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], parts_[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manage_rights\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                query.edit_message_text(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.toggle_admin_right(target_id, right_key)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Если изменяется право с суффиксом _edit и новое значение True, устанавливаем соответствующее базовое право</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> right_key.endswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"_edit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_status:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                base_key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> right_key[:</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                rights </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> load_admin_rights()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                rights[_norm_key(target_id)][base_key] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                save_admin_rights(rights)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Обновляем список команд для пользователя, у которого изменились права</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            target_num </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _numeric_uid(target_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> right_key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ai_chat\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"opencode_chat\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"bot_status\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target_num:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.set_commands(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">user_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">target_num)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Отправляем уведомление администратору о смене рабочего значения (для включения и выключения)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> target_num:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.send_message(</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                        chat_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">target_num,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                        text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ваше право '</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">right_key</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">' было переключено на </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'✅'</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '❌'</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    )</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка отправки уведомления об изменении права '</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">right_key</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">' для администратора </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">target_id</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            query.answer(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">right_key</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> переключено на </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'✅'</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '❌'</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> для ID </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">target_id</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">show_alert</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Если переключается право уведомлений, остаёмся в подменю уведомлений, иначе возвращаемся к основному меню прав</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> right_key.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_notifications_rights_menu(chat_id, target_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_rights_menu_for_admin(chat_id, target_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Обработка подменю уведомлений:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"bot_notifications\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_notifications_menu(chat_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"toggle_no_sound_notification\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.toggle_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_no_sound\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.answer_callback_query(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_query_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">query.id, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Отправлять без звука переключено на </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'✅'</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '❌'</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">show_alert</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_notifications_menu(chat_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"toggle_nmap_notification\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.toggle_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_nmap\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.answer_callback_query(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_query_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">query.id, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Уведомления Nmap переключены на </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'✅'</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '❌'</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">show_alert</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_notifications_menu(chat_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"toggle_guacamole_notification\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.toggle_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_guacamole\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.answer_callback_query(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_query_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">query.id, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Уведомления Guacamole переключены на </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'✅'</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '❌'</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">show_alert</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_notifications_menu(chat_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"toggle_f2b_notification\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.toggle_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_f2b\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.answer_callback_query(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_query_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">query.id, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Уведомления F2B переключены на </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'✅'</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '❌'</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">show_alert</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_notifications_menu(chat_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"toggle_wireguard_notification\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.toggle_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_wireguard\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.answer_callback_query(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_query_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">query.id, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Уведомления Wireguard переключены на </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'✅'</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '❌'</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">show_alert</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_notifications_menu(chat_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"toggle_status_notification\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.toggle_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_status\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.answer_callback_query(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_query_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">query.id, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Уведомления Status переключены на </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'✅'</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '❌'</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">show_alert</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_notifications_menu(chat_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"toggle_new_user_notification\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.toggle_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_new_user\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.answer_callback_query(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_query_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">query.id, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Уведомления о новых пользователях переключены на </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'✅'</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '❌'</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">show_alert</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_notifications_menu(chat_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"toggle_zabbix_notification\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.toggle_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_zabbix\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.dispatcher.bot.answer_callback_query(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_query_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">query.id, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Уведомления Zabbix переключены на </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'✅'</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '❌'</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">show_alert</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_notifications_menu(chat_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"get_latest_messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_latest_messages(chat_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # service_* обслуживает модуль services (ui_tg, группа -1).</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"noop\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            query.answer()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"my_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            query.edit_message_text(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ваш ID: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user_id</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"console_session\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.start_console_session(chat_id, context, user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            query.edit_message_text(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Неизвестная команда.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> show_admin_selection_menu</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, chat_id, context, user_id):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manage_rights\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        rights </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> load_admin_rights()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        admin_ids </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(_norm_key(x) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _admin_ids()) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(rights.keys())</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        buttons </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> aid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(admin_ids):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            buttons.append(InlineKeyboardButton(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"👤 ID: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">aid</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"select_admin_rights:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">aid</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        rows </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [buttons[i:i</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> i </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> range</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(buttons), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        rows.append([InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔙 Назад\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"main_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        reply_markup </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardMarkup(rows)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🛠 Выберите администратора для управления правами:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">reply_markup</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">reply_markup, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> show_rights_menu_for_admin</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, chat_id, target_id, context, user_id):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.get_admin_right(user_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manage_rights\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        rights </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> load_admin_rights()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        uid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _norm_key(target_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> uid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rights:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            rights[uid] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _access().guest_defaults()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            save_admin_rights(rights)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Подтянем новые ключи из реестра ядра (динамика)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        changed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k, v </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _access().guest_defaults().items():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rights[uid]:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                rights[uid][k] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> v</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                changed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> changed:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            save_admin_rights(rights)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user_rights </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rights[uid]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        text_lines </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🛠 Права для ID: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">uid</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Разбиваем ключи на общие и уведомлений (те, что начинаются с \"receive_notifications_\")</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        general_keys </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user_rights.keys() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"pve_manage_hosts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        notif_keys </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user_rights.keys() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        buttons </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Формируем кнопки для общих прав</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> general_keys:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            allowed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user_rights.get(key, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            emoji </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"✅\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> allowed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"❌\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            buttons.append(InlineKeyboardButton(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">key.capitalize()</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> {</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">emoji</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toggle:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">uid</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">key</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Добавляем отдельную кнопку для перехода в подменю прав уведомлений</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> notif_keys:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            buttons.append(InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔔 Права уведомлений\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"notif_rights_menu:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">uid</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Добавляем кнопку для прав PVE</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        buttons.append(InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🏠 Права PVE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"pve_rights_menu:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">uid</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        rows </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [buttons[i:i</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> i </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> range</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(buttons), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        rows.append([InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔙 Назад\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"rights_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                       InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🏠 Главное меню\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"main_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        reply_markup </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardMarkup(rows)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        text </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(text_lines)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, text, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">reply_markup</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">reply_markup, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> show_notifications_rights_menu</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, chat_id, target_id, context, user_id):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        rights </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> load_admin_rights()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        uid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _norm_key(target_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> uid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rights:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            rights[uid] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _access().guest_defaults()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            save_admin_rights(rights)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Подтянем новые ключи из реестра ядра (динамика)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        changed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k, v </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _access().guest_defaults().items():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rights[uid]:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                rights[uid][k] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> v</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                changed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> changed:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            save_admin_rights(rights)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user_rights </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rights[uid]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        text_lines </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔔 Права уведомлений для ID: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">uid</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Выбираем только ключи, начинающиеся с \"receive_notifications_\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        notif_keys </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user_rights.keys() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        buttons </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> notif_keys:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            allowed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user_rights.get(key, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            emoji </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"✅\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> allowed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"❌\"</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Для наглядности убираем префикс в названии кнопки</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            display_name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key.replace(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).capitalize()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            buttons.append(InlineKeyboardButton(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">display_name</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> {</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">emoji</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toggle:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">uid</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">key</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        rows </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [buttons[i:i</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> i </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> range</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(buttons), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Кнопка возврата в основное меню прав</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        rows.append([InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔙 Назад\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toggle_return:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">uid</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        reply_markup </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardMarkup(rows)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        text </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(text_lines)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, text, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">reply_markup</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">reply_markup, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> handle_text</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, update, context):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chat_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.message.chat.id</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        message_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.message.message_id</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_user</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user.id</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        text </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.message.text </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Режим OpenCode (модуль opencode_chat, группа -2) имеет приоритет:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # если у пользователя активна сессия opencode — пропускаем обработку здесь.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._opencode_active(user_id):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> DispatcherHandlerStop()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        logger.info(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"AdminMenu: User </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (ID: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) отправил текст в меню: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, user.username, user_id, text)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # для групп сохраняем с chat_id</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            context._group_log_chat </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: update.effective_chat.id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: update.effective_chat.type, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(update.effective_chat, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'title'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">''</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.log_user_message(user, update.message.text, context)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        state </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> context.chat_data.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"awaiting\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            context.chat_data.pop(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"awaiting\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"add_admin\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.process_add_admin(update, context, update.message.text)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"remove_admin\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.process_remove_admin(update, context, update.message.text)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"script_withparams\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                script_name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> context.chat_data.pop(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"script_name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.run_script_common(chat_id, context, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._scripts_dir(), script_name, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"run_scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">params</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">update.message.text, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">user_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Неожиданный ввод.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            </span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                context.bot.delete_message(chat_id, message_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка удаления сообщения: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        </span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> user_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.console_sessions:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.process_console_command(chat_id, context, user_id, update.message.text)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                context.bot.delete_message(chat_id, message_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка удаления сообщения: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        </span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # В группах не спамим \"Нераспознанная команда\" — мешает групповому чату (реплаи/! триггеры)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_chat.type </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"private\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Если ни одно из условий не выполнено, выводим сообщение об ошибке.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_msg(chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Нераспознанная команда. Используйте меню через /start.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">context</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            context.bot.delete_message(chat_id, message_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка удаления сообщения: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _opencode_active</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, user_id):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"True, если у пользователя активна сессия opencode_chat.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        refs </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"_module_refs\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(refs, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        oc </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> refs.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"opencode_chat\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(oc </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(oc, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"is_active\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> oc.is_active(user_id))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> error_handler</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, update, context):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.message:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            user </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.message.from_user</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка от </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user.username</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user.id</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">): </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">context.error</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "manifest.yaml",
          "size": 2990,
          "content": "name: admin\nversion: 0.2.2\ncategory: base\ndescription: \"Админ-ядро TG (AdminMenu): главное меню, права, скрипты, журнал, бэкапы.\"\napi:\n  - admin.menu\n  - admin.toggle_right\n  - admin.journal\napi_methods:\n  - method: menu\n    args: \"{}\"\n    returns: \"AdminMenu — вендорный движок (register_handlers, show_main_menu, console_sessions)\"\n    desc: Доступ к движку админ-меню (тот же объект, что обслуживает TG-хендлеры)\n  - method: toggle_right\n    args: \"{key: 'telegram|vk|web:<id>', right: str}\"\n    returns: \"bool — новое значение\"\n    desc: Переключить право (семантика AdminMenu.toggle_admin_right)\n  - method: journal\n    args: \"{limit: int = 200}\"\n    returns: \"list — хвост журнала сообщений пользователей\"\n    desc: Читать журнал user_messages.json (кто/что/когда, включая группы)\nevents_emitted: []\nevents_subscribed: []\naudit_allow:\n  - JS_INNERHTML\n  - RM\n  - SUBPROC_SHELL\n\nrights:\n  - name: restart_bot\n    description: Перезапуск службы бота (POST /api/admin/system action=restart_bot)\n  - name: restart_machine\n    description: Перезагрузка хоста (POST /api/admin/system action=restart_machine)\n  - name: shutdown_host\n    description: Выключение хоста (POST /api/admin/system action=shutdown)\n  - name: manage_admins\n    description: Назначение/снятие TG-админов (POST /api/admin/admins)\n  - name: receive_notifications_new_user\n    description: \"Уведомления о новых пользователях (модуль admin)\"\n    all_ignore: true\n  - name: receive_notifications_new_user_edit\n    description: \"Право менять себе уведомления о новых пользователях\"\n    all_ignore: true\n  - name: run_scripts\n    description: Запуск скриптов обслуживания (GET/POST /api/admin/scripts)\n  - name: view_user_messages\n    description: Журнал сообщений пользователей (GET /api/admin/messages)\n  - name: menu_edit\n    description: Раскладка меню /start (GET/POST /api/admin/tgmenu)\n    description: Группы Telegram (TG-команды group*, /api/admin/group*)\nuses_rights:\n  - receive_notifications\ntg_commands:\n  - start\n  - id\nweb_routes:\n  - POST /api/admin/admins\n  - POST /api/admin/system\n  - GET /api/admin/scripts\n  - POST /api/admin/scripts\n  - GET /api/admin/messages\n  - GET /api/admin/group\n  - POST /api/admin/group\n  - GET /api/admin/group/messages\n  - GET /api/admin/tgmenu\n  - POST /api/admin/tgmenu\nshared_routes:\n  - /api/admin/admins\n  - /api/admin/system\n  - /api/admin/scripts\n  - /api/admin/messages\n  - /api/admin/group\n  - /api/admin/group/messages\n  - /api/admin/tgmenu\nconfig_schema: config.schema.json\nenv_example: .env.example\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">admin</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">version</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0.2.2</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">category</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">base</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Админ-ядро TG (AdminMenu): главное меню, права, скрипты, журнал, бэкапы.\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">admin.menu</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">admin.toggle_right</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">admin.journal</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">api_methods</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">menu</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"AdminMenu — вендорный движок (register_handlers, show_main_menu, console_sessions)\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Доступ к движку админ-меню (тот же объект, что обслуживает TG-хендлеры)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">toggle_right</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{key: 'telegram|vk|web:&#x3C;id>', right: str}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"bool — новое значение\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Переключить право (семантика AdminMenu.toggle_admin_right)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">journal</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{limit: int = 200}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"list — хвост журнала сообщений пользователей\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Читать журнал user_messages.json (кто/что/когда, включая группы)</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">events_emitted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: []</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">events_subscribed</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: []</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">audit_allow</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">JS_INNERHTML</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">RM</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">SUBPROC_SHELL</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">rights</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">restart_bot</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Перезапуск службы бота (POST /api/admin/system action=restart_bot)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">restart_machine</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Перезагрузка хоста (POST /api/admin/system action=restart_machine)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">shutdown_host</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Выключение хоста (POST /api/admin/system action=shutdown)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">manage_admins</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Назначение/снятие TG-админов (POST /api/admin/admins)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">receive_notifications_new_user</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Уведомления о новых пользователях (модуль admin)\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    all_ignore</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">true</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">receive_notifications_new_user_edit</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Право менять себе уведомления о новых пользователях\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    all_ignore</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">true</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">run_scripts</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Запуск скриптов обслуживания (GET/POST /api/admin/scripts)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">view_user_messages</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Журнал сообщений пользователей (GET /api/admin/messages)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">menu_edit</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Раскладка меню /start (GET/POST /api/admin/tgmenu)</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Группы Telegram (TG-команды group*, /api/admin/group*)</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">uses_rights</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">receive_notifications</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">tg_commands</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">start</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">id</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">web_routes</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">POST /api/admin/admins</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">POST /api/admin/system</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/admin/scripts</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">POST /api/admin/scripts</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/admin/messages</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/admin/group</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">POST /api/admin/group</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/admin/group/messages</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/admin/tgmenu</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">POST /api/admin/tgmenu</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">shared_routes</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/api/admin/admins</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/api/admin/system</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/api/admin/scripts</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/api/admin/messages</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/api/admin/group</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/api/admin/group/messages</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/api/admin/tgmenu</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">config_schema</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">config.schema.json</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">env_example</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.env.example</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "module.py",
          "size": 5335,
          "content": "\"\"\"admin.Module — вендор AdminMenu (TG-меню, права, скрипты, журнал, бэкапы).\n\nLegacy-режим (как ai_chat): engine регистрирует PTB-хендлеры сам\n(/start, /id, кнопки, текстовый журнал сообщений). Состояние —\ndata/: user_messages.json (журнал, ОТДЕЛЬНЫЙ от прод-бота: общий файл\nдал бы гонку записей), console_sessions.log. Права — ОБЩИЙ\ndata/admin_rights.json (ядро перечитывает по mtime).\n\"\"\"\nimport logging\nimport os\n\nfrom core.base_module import BaseModule\nfrom core.legacy import AdminRightsShim\n\nfrom . import config as pkg_config\n\nlogger = logging.getLogger(__name__)\n\n\nclass Module(BaseModule):\n    def setup(self, ctx) -> None:\n        mod_dir = os.path.dirname(os.path.abspath(__file__))\n        self.config = ctx.module_config(\"admin\", mod_dir)\n        self.ctx = ctx\n        net_dir = os.path.join(ctx.data_dir, \"cache\", \"network\")\n\n        def _f(key, default):\n            return (self.config.get(key, \"\") or \"\").strip() or default\n\n        def _fp(key, default):\n            from core.paths import abs_data\n            return abs_data(ctx.data_dir,\n                            (self.config.get(key, \"\") or \"\").strip(),\n                            default)\n\n        files = {\n            \"ADMIN_RIGHTS_FILE\": os.path.join(ctx.data_dir, \"admin_rights.json\"),\n            \"CACHE_USER_MESSAGES\": _fp(\n                \"CACHE_USER_MESSAGES\", os.path.join(ctx.data_dir, \"user_messages.json\")),\n            \"CACHE_FILE_NETWORK\": _fp(\n                \"CACHE_FILE_NETWORK\", os.path.join(net_dir, \"networkscan_cache.json\")),\n            \"SCAN_NETWORK_IGNORED_MACS\": _fp(\n                \"SCAN_NETWORK_IGNORED_MACS\", os.path.join(net_dir, \"networkscan_ignored.json\")),\n            \"LOG_CONSOLE\": _fp(\n                \"LOG_CONSOLE\", os.path.join(ctx.data_dir, \"console_sessions.log\")),\n        }\n        pkg_config.install(dict(self.config.values), files)\n\n        # Скрипты scripts_admin/ читают IDE/PASS из окружения процесса\n        # (старый бот получал их через EnvironmentFile юнита). Экспортируем\n        # только то, что нужно скриптам, без перезаписи уже заданного.\n        for key in (\"REMOTE_PASSWORD_WIN\", \"REMOTE_PASSWORD_DOCKER\",\n                    \"LOCAL_PASSWORD\", \"REMOTE_HOST_WIN\", \"REMOTE_USER_WIN\",\n                    \"REMOTE_HOST_DOCKER\", \"REMOTE_USER_DOCKER\"):\n            val = (self.config.get(key, \"\") or \"\").strip()\n            if val and not os.environ.get(key):\n                os.environ[key] = val\n\n        try:\n            from . import engine as engine_mod\n        except ImportError as e:\n            from core.errors import ConfigError\n            raise ConfigError(\"admin: движок требует venv (telegram/pexpect): %s\" % e)\n        self.menu = engine_mod.AdminMenu(dispatcher=None)\n        from . import tgmenu as _tgmenu\n        _tglog = ctx.get_logger(\"admin\")\n\n        def _menu_spec(user_id):\n            catalog = _tgmenu.collect_buttons(ctx.loaded_modules, _tglog)\n            layout = _tgmenu.load_layout(ctx.data_dir, catalog)\n            return _tgmenu.visible_buttons(\n                catalog, layout, self.menu.get_admin_right, user_id)\n\n        self.menu.tgmenu_provider = _menu_spec\n        self._shim = AdminRightsShim(ctx.rights)\n        ctx.api.register(\"admin\", \"menu\", lambda: self.menu)\n        ctx.api.register(\n            \"admin\", \"toggle_right\",\n            lambda key, right, actor=\"\": self._toggle_right(\n                key, right, actor))\n        ctx.api.register(\n            \"admin\", \"journal\",\n            lambda limit=200: self._journal(limit))\n\n    def _toggle_right(self, key, right, actor=\"\"):\n        \"\"\"Переключить право key.right через RightsService (возврат bool).\"\"\"\n        import re as _re\n        if not _re.match(r\"^(telegram|vk|web):\\d+$\", key or \"\"):\n            raise ValueError(\"Некорректный ключ пользователя\")\n        value = self.ctx.rights.toggle_right(key, right, actor=actor)\n        # notify пользователю о смене его права (как в проде).\n        try:\n            self.ctx.api.call(\"notify.rights_changed\", key, right, value)\n        except Exception:  # noqa: BLE001\n            pass\n        return value\n\n    def _journal(self, limit=200):\n        \"\"\"Хвост журнала сообщений пользователей [{user_id,name,text,time,...}].\"\"\"\n        from . import engine as engine_mod\n        try:\n            cache = engine_mod.load_user_message_cache()\n        except Exception:  # noqa: BLE001\n            return []\n        msgs = (cache or {}).get(\"messages\") or []\n        try:\n            limit = max(1, min(int(limit), 2000))\n        except (ValueError, TypeError):\n            limit = 200\n        return msgs[-limit:]\n\n    def health(self) -> dict:\n        menu = getattr(self, \"menu\", None)\n        try:\n            n = len(getattr(menu, \"console_sessions\", {}) or {})\n        except Exception:  # noqa: BLE001\n            n = -1\n        return {\"ok\": menu is not None, \"module\": self.name, \"consoles\": n}\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"admin.Module — вендор AdminMenu (TG-меню, права, скрипты, журнал, бэкапы).</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Legacy-режим (как ai_chat): engine регистрирует PTB-хендлеры сам</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(/start, /id, кнопки, текстовый журнал сообщений). Состояние —</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">data/: user_messages.json (журнал, ОТДЕЛЬНЫЙ от прод-бота: общий файл</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">дал бы гонку записей), console_sessions.log. Права — ОБЩИЙ</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">data/admin_rights.json (ядро перечитывает по mtime).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.base_module </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> BaseModule</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.legacy </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> AdminRightsShim</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pkg_config</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">logger </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging.getLogger(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__name__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">class</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Module</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">BaseModule</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> setup</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, ctx) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mod_dir </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.dirname(os.path.abspath(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__file__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.module_config(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"admin\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, mod_dir)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        net_dir </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(ctx.data_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cache\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _f</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(key, default):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.get(key, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> default</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _fp</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(key, default):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.paths </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> abs_data</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> abs_data(ctx.data_dir,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                            (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.get(key, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip(),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                            default)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        files </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"ADMIN_RIGHTS_FILE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: os.path.join(ctx.data_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"admin_rights.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"CACHE_USER_MESSAGES\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _fp(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"CACHE_USER_MESSAGES\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, os.path.join(ctx.data_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"user_messages.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"CACHE_FILE_NETWORK\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _fp(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"CACHE_FILE_NETWORK\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, os.path.join(net_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"networkscan_cache.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"SCAN_NETWORK_IGNORED_MACS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _fp(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"SCAN_NETWORK_IGNORED_MACS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, os.path.join(net_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"networkscan_ignored.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"LOG_CONSOLE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _fp(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"LOG_CONSOLE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, os.path.join(ctx.data_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"console_sessions.log\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        pkg_config.install(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.values), files)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Скрипты scripts_admin/ читают IDE/PASS из окружения процесса</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # (старый бот получал их через EnvironmentFile юнита). Экспортируем</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # только то, что нужно скриптам, без перезаписи уже заданного.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"REMOTE_PASSWORD_WIN\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"REMOTE_PASSWORD_DOCKER\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"LOCAL_PASSWORD\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"REMOTE_HOST_WIN\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"REMOTE_USER_WIN\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"REMOTE_HOST_DOCKER\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"REMOTE_USER_DOCKER\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            val </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.get(key, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> val </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.environ.get(key):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                os.environ[key] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> val</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> engine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> engine_mod</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ImportError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.errors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ConfigError</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ConfigError(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"admin: движок требует venv (telegram/pexpect): </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.menu </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> engine_mod.AdminMenu(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">dispatcher</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tgmenu </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _tgmenu</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        _tglog </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.get_logger(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"admin\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _menu_spec</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(user_id):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            catalog </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _tgmenu.collect_buttons(ctx.loaded_modules, _tglog)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            layout </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _tgmenu.load_layout(ctx.data_dir, catalog)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _tgmenu.visible_buttons(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                catalog, layout, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.menu.get_admin_right, user_id)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.menu.tgmenu_provider </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _menu_spec</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._shim </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> AdminRightsShim(ctx.rights)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"admin\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">lambda</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.menu)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"admin\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toggle_right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            lambda</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key, right, actor</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._toggle_right(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                key, right, actor))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"admin\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"journal\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            lambda</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> limit</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">200</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._journal(limit))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _toggle_right</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, key, right, actor</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Переключить право key.right через RightsService (возврат bool).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re.match(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">^(</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">telegram</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">vk</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">web</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">)</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\d</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">$</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Некорректный ключ пользователя\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx.rights.toggle_right(key, right, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">actor</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">actor)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # notify пользователю о смене его права (как в проде).</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"notify.rights_changed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, key, right, value)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> value</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _journal</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, limit</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">200</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Хвост журнала сообщений пользователей [{user_id,name,text,time,...}].\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> engine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> engine_mod</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> engine_mod.load_user_message_cache()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        msgs </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}).get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            limit </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> max</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">min</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(limit), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2000</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TypeError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            limit </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 200</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgs[</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">limit:]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> health</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        menu </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            n </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(menu, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"console_sessions\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {}) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {})</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            n </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> -</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: menu </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"module\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.name, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"consoles\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: n}</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "requirements.txt",
          "size": 337,
          "content": "# Зависимости admin:\n# pexpect — запуск скриптов services (engine.py).\n# Остальное — только для РУЧНОГО скрипта\n# scripts_admin/miio_cloud_extract.py (выгрузка токенов из облака Xiaomi).\npexpect==4.9.0\nptyprocess==0.7.0\npython-miio==0.5.12\nmicloud==0.6\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span># Зависимости admin:</span></span>\n<span class=\"line\"><span># pexpect — запуск скриптов services (engine.py).</span></span>\n<span class=\"line\"><span># Остальное — только для РУЧНОГО скрипта</span></span>\n<span class=\"line\"><span># scripts_admin/miio_cloud_extract.py (выгрузка токенов из облака Xiaomi).</span></span>\n<span class=\"line\"><span>pexpect==4.9.0</span></span>\n<span class=\"line\"><span>ptyprocess==0.7.0</span></span>\n<span class=\"line\"><span>python-miio==0.5.12</span></span>\n<span class=\"line\"><span>micloud==0.6</span></span>\n<span class=\"line\"><span></span></span></code></pre>"
        },
        {
          "path": "tgmenu.py",
          "size": 7089,
          "content": "\"\"\"admin.tgmenu — динамическое меню /start.\n\nМодули заявляют кнопки верхнего уровня через TG_MENU в своём ui_tg:\n\n    TG_MENU = [{\"id\": \"pve\", \"title\": \"🏠 PVE\", \"callback\": \"pve_menu\",\n                \"right\": \"pve_manage\"}]\n\nright: None (все) | одно право | \"a;b;...\" (ИЛИ). Раскладка data/tg_menu.json:\nпорядок, wide (своя строка), hidden. Правка — /api/admin/tgmenu (menu_edit).\nКолбэки обслуживают сами модули либо legacy-движок admin (pve_menu и др.).\n\"\"\"\nimport json\nimport logging\nimport os\n\nlogger = logging.getLogger(__name__)\n\nLAYOUT_FILE = \"tg_menu.json\"\nLAYOUT_MAX_BUTTONS = 100\n# Доли строки в TG-меню. auto — поток по 2 в ряд (как раньше), full — своя строка.\nLAYOUT_WIDTHS = (\"auto\", \"1/4\", \"2/4\", \"3/4\", \"1/3\", \"2/3\", \"full\")\n\n\ndef _valid_entry(entry) -> bool:\n    return (isinstance(entry, dict) and isinstance(entry.get(\"id\"), str)\n            and entry[\"id\"] and isinstance(entry.get(\"title\"), str)\n            and isinstance(entry.get(\"callback\"), str))\n\n\ndef collect_buttons(loaded_modules, log=None):\n    \"\"\"Каталог кнопок загруженных модулей (порядок каталога = порядок модулей).\"\"\"\n    out = []\n    seen = set()\n    import importlib as _il\n    for mod in loaded_modules or []:\n        try:\n            ui = _il.import_module(\"botmod_%s.ui_tg\" % mod.name)\n        except Exception:  # noqa: BLE001\n            continue\n        raw = getattr(ui, \"TG_MENU\", None)\n        if not raw:\n            continue\n        items = raw if isinstance(raw, list) else [raw]\n        for item in items:\n            if not _valid_entry(item):\n                (log.info if log else (lambda *a: None))(\n                    \"модуль %s: битая кнопка меню: %r\", mod.name, item)\n                continue\n            bid = item[\"id\"].strip()\n            if bid in seen:\n                (log.warning if log else (lambda *a: None))(\n                    \"модуль %s: кнопка %s — дубль id, пропущена\", mod.name, bid)\n                continue\n            seen.add(bid)\n            out.append({\n                \"id\": bid,\n                \"title\": item[\"title\"].strip()[:64],\n                \"callback\": item[\"callback\"].strip()[:64],\n                \"right\": item.get(\"right\"),\n                \"module\": mod.name,\n            })\n    return out\n\n\ndef _layout_path(data_dir):\n    return os.path.join(data_dir, LAYOUT_FILE)\n\n\ndef _valid_width(value) -> bool:\n    return value in LAYOUT_WIDTHS\n\n\ndef _entry_width(entry) -> str:\n    if isinstance(entry, dict):\n        w = entry.get(\"width\") or (\"full\" if entry.get(\"wide\") else \"auto\")\n        if _valid_width(w):\n            return w\n    return \"auto\"\n\n\ndef _entry_title(entry):\n    if isinstance(entry, dict):\n        t = entry.get(\"title\")\n        if isinstance(t, str) and t.strip():\n            return t.strip()[:64]\n    return None\n\n\ndef load_layout(data_dir, catalog):\n    \"\"\"Раскладка, сведённая с каталогом: [{id, width, hidden, title}].\"\"\"\n    known = {b[\"id\"] for b in catalog}\n    try:\n        with open(_layout_path(data_dir), \"r\", encoding=\"utf-8\") as f:\n            saved = json.load(f) or {}\n    except Exception:  # noqa: BLE001\n        saved = {}\n    order, flags, seen = [], {}, set()\n    for entry in (saved.get(\"buttons\") or []):\n        if not isinstance(entry, dict):\n            continue\n        bid = entry.get(\"id\")\n        if bid not in known or bid in seen:\n            continue\n        seen.add(bid)\n        flags[bid] = {\"width\": _entry_width(entry),\n                      \"hidden\": bool(entry.get(\"hidden\", False)),\n                      \"title\": _entry_title(entry)}\n        order.append(bid)\n    for b in catalog:\n        if b[\"id\"] not in seen:\n            flags[b[\"id\"]] = {\"width\": \"auto\", \"hidden\": False, \"title\": None}\n            order.append(b[\"id\"])\n    return [{\"id\": bid, **flags[bid]} for bid in order]\n\n\ndef save_layout(data_dir, catalog, entries):\n    \"\"\"Проверить и сохранить раскладку. Вернуть (ok, error).\"\"\"\n    known = {b[\"id\"] for b in catalog}\n    if not isinstance(entries, list) or len(entries) > LAYOUT_MAX_BUTTONS:\n        return False, \"buttons должен быть списком ≤%d\" % LAYOUT_MAX_BUTTONS\n    seen, clean = set(), []\n    for entry in entries:\n        if not isinstance(entry, dict):\n            return False, \"запись кнопки должна быть объектом\"\n        bid = entry.get(\"id\")\n        if bid not in known or bid in seen:\n            return False, \"неизвестная или повторная кнопка: %r\" % (bid,)\n        seen.add(bid)\n        raw_width = entry.get(\"width\") if isinstance(entry, dict) else None\n        if raw_width is None and isinstance(entry, dict) and entry.get(\"wide\"):\n            raw_width = \"full\"\n        width = raw_width or \"auto\"\n        if not _valid_width(width):\n            return False, \"некорректная ширина кнопки %r\" % (bid,)\n        raw_title = entry.get(\"title\") if isinstance(entry, dict) else None\n        if raw_title is not None and (\n                not isinstance(raw_title, str) or not raw_title.strip()\n                or len(raw_title.strip()) > 64):\n            return False, \"некорректное название кнопки %r (1..64 символа)\" % (bid,)\n        clean.append({\"id\": bid, \"width\": width,\n                      \"hidden\": bool(entry.get(\"hidden\", False)),\n                      \"title\": raw_title.strip()[:64] if isinstance(raw_title, str) and raw_title.strip() else None})\n    try:\n        tmp = _layout_path(data_dir) + \".tmp\"\n        with open(tmp, \"w\", encoding=\"utf-8\") as f:\n            json.dump({\"buttons\": clean}, f, ensure_ascii=False, indent=2)\n        os.replace(tmp, _layout_path(data_dir))\n    except Exception as e:  # noqa: BLE001\n        return False, \"ошибка записи: %s\" % e\n    return True, None\n\n\ndef has_any_right(check, user_id, right) -> bool:\n    \"\"\"Право кнопки: None — все; 'a;b' — ИЛИ.\"\"\"\n    if not right:\n        return True\n    for name in str(right).split(\";\"):\n        name = name.strip()\n        if name and check(user_id, name):\n            return True\n    return False\n\n\ndef visible_buttons(catalog, layout, check, user_id):\n    \"\"\"Кнопки для пользователя: порядок+wide из layout, фильтр прав.\"\"\"\n    by_id = {b[\"id\"]: b for b in catalog}\n    out = []\n    for entry in layout:\n        btn = by_id.get(entry.get(\"id\"))\n        if btn is None or entry.get(\"hidden\"):\n            continue\n        if not has_any_right(check, user_id, btn.get(\"right\")):\n            continue\n        btn = {**btn, \"width\": _entry_width(entry)}\n        title = _entry_title(entry)\n        if title:\n            btn[\"title\"] = title\n        out.append(btn)\n    return out\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"admin.tgmenu — динамическое меню /start.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Модули заявляют кнопки верхнего уровня через TG_MENU в своём ui_tg:</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    TG_MENU = [{\"id\": \"pve\", \"title\": \"🏠 PVE\", \"callback\": \"pve_menu\",</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"right\": \"pve_manage\"}]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">right: None (все) | одно право | \"a;b;...\" (ИЛИ). Раскладка data/tg_menu.json:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">порядок, wide (своя строка), hidden. Правка — /api/admin/tgmenu (menu_edit).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Колбэки обслуживают сами модули либо legacy-движок admin (pve_menu и др.).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">logger </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging.getLogger(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__name__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">LAYOUT_FILE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"tg_menu.json\"</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">LAYOUT_MAX_BUTTONS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 100</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Доли строки в TG-меню. auto — поток по 2 в ряд (как раньше), full — своя строка.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">LAYOUT_WIDTHS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"auto\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"2/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"3/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1/3\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"2/3\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"full\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _valid_entry</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entry) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entry, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entry.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entry[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entry.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            and</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entry.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"callback\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> collect_buttons</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(loaded_modules, log</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Каталог кнопок загруженных модулей (порядок каталога = порядок модулей).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    out </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    seen </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> importlib </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _il</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mod </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> loaded_modules </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ui </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _il.import_module(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"botmod_</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.ui_tg\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mod.name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        raw </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ui, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"TG_MENU\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> raw:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        items </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> raw </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(raw, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">list</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [raw]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> item </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> items:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _valid_entry(item):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                (log.info </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> log </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">lambda</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> *</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">a: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"модуль </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: битая кнопка меню: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, mod.name, item)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            bid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> item[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> bid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> seen:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                (log.warning </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> log </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">lambda</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> *</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">a: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"модуль </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: кнопка </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> — дубль id, пропущена\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, mod.name, bid)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            seen.add(bid)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out.append({</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: bid,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: item[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].strip()[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">64</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"callback\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: item[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"callback\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].strip()[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">64</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: item.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"module\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: mod.name,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            })</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> out</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _layout_path</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data_dir):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(data_dir, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">LAYOUT_FILE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _valid_width</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(value) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> LAYOUT_WIDTHS</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _entry_width</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entry) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entry, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        w </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entry.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"width\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"full\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entry.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"wide\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"auto\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _valid_width(w):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> w</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"auto\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _entry_title</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entry):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entry, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        t </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entry.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(t, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> t.strip():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> t.strip()[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">64</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> load_layout</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data_dir, catalog):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Раскладка, сведённая с каталогом: [{id, width, hidden, title}].\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    known </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {b[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> catalog}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(_layout_path(data_dir), </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"r\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            saved </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json.load(f) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        saved </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    order, flags, seen </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [], {}, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entry </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (saved.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"buttons\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entry, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        bid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entry.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> bid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> known </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> bid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> seen:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        seen.add(bid)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        flags[bid] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"width\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _entry_width(entry),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                      \"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entry.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                      \"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _entry_title(entry)}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        order.append(bid)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> catalog:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> seen:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            flags[b[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"width\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"auto\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            order.append(b[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: bid, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">**</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">flags[bid]} </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> bid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> order]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> save_layout</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data_dir, catalog, entries):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Проверить и сохранить раскладку. Вернуть (ok, error).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    known </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {b[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> catalog}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entries, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">list</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entries) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> LAYOUT_MAX_BUTTONS</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"buttons должен быть списком ≤</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> LAYOUT_MAX_BUTTONS</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    seen, clean </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(), []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entry </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entries:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entry, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"запись кнопки должна быть объектом\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        bid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entry.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> bid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> known </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> bid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> seen:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"неизвестная или повторная кнопка: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (bid,)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        seen.add(bid)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        raw_width </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entry.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"width\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entry, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> raw_width </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> and</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entry, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entry.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"wide\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            raw_width </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"full\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        width </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> raw_width </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"auto\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _valid_width(width):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"некорректная ширина кнопки </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (bid,)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        raw_title </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entry.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entry, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> raw_title </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(raw_title, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> raw_title.strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                or</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(raw_title.strip()) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 64</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"некорректное название кнопки </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (1..64 символа)\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (bid,)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        clean.append({</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: bid, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"width\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: width,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                      \"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(entry.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                      \"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: raw_title.strip()[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">64</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(raw_title, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> raw_title.strip() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">})</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        tmp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _layout_path(data_dir) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \".tmp\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(tmp, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"w\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            json.dump({</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"buttons\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: clean}, f, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">ensure_ascii</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">indent</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        os.replace(tmp, _layout_path(data_dir))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ошибка записи: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> has_any_right</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(check, user_id, right) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Право кнопки: None — все; 'a;b' — ИЛИ.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> right:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(right).split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\";\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name.strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> check(user_id, name):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> visible_buttons</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(catalog, layout, check, user_id):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Кнопки для пользователя: порядок+wide из layout, фильтр прав.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    by_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {b[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]: b </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> catalog}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    out </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entry </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> layout:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        btn </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> by_id.get(entry.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> btn </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> entry.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> has_any_right(check, user_id, btn.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        btn </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">**</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">btn, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"width\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _entry_width(entry)}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        title </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _entry_title(entry)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> title:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            btn[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> title</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out.append(btn)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> out</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "ui_tg.py",
          "size": 1431,
          "content": "\"\"\"admin.ui_tg — LEGACY: AdminMenu регистрирует PTB-хендлеры сам.\n\nЗдесь только декларации для /menu и прав + хук register_ptb.\nТекстовый хендлер движка (журнал сообщений) — внутри engine.\n\"\"\"\n\nTG_COMMANDS = {\n    \"start\": {\"desc\": \"Главное меню\", \"right\": None, \"menu\": True},\n    \"id\": {\"desc\": \"Получить свой ID\", \"right\": None, \"menu\": True},\n}\n\n# Кнопки /start от admin (колбэки обслуживает engine ниже).\nTG_MENU = [\n    {\"id\": \"admin_main\", \"title\": \"👮 Администрирование\", \"callback\": \"admin_menu\",\n     \"right\": \"manage_admins\"},\n    {\"id\": \"bot_ctrl\", \"title\": \"🤖 Управление ботом\", \"callback\": \"bot_menu\",\n     \"right\": \"restart_bot;restart_machine;shutdown_host;view_user_messages\"},\n    {\"id\": \"scripts\", \"title\": \"📜 Скрипты\", \"callback\": \"script_menu\",\n     \"right\": \"run_scripts\"},\n    {\"id\": \"notify_setup\", \"title\": \"🔔 Уведомления\", \"callback\": \"bot_notifications\",\n     \"right\": \"receive_notifications\"},\n    {\"id\": \"my_id\", \"title\": \"🆔 Мой ID\", \"callback\": \"my_id\",\n     \"right\": None},\n]\n\n\ndef register_ptb(facade, mod):\n    \"\"\"Legacy-хук: движок регистрируется на фасаде напрямую.\"\"\"\n    mod.menu.dispatcher = facade\n    mod.menu.register_handlers()\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"admin.ui_tg — LEGACY: AdminMenu регистрирует PTB-хендлеры сам.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Здесь только декларации для /menu и прав + хук register_ptb.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Текстовый хендлер движка (журнал сообщений) — внутри engine.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TG_COMMANDS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"start\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Главное меню\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Получить свой ID\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Кнопки /start от admin (колбэки обслуживает engine ниже).</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TG_MENU</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"admin_main\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"👮 Администрирование\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"callback\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"admin_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manage_admins\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"bot_ctrl\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🤖 Управление ботом\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"callback\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"bot_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart_bot;restart_machine;shutdown_host;view_user_messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📜 Скрипты\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"callback\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"script_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"run_scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"notify_setup\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔔 Уведомления\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"callback\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"bot_notifications\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"my_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🆔 Мой ID\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"callback\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"my_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> register_ptb</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(facade, mod):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Legacy-хук: движок регистрируется на фасаде напрямую.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    mod.menu.dispatcher </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> facade</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    mod.menu.register_handlers()</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "ui_web.py",
          "size": 17659,
          "content": "\"\"\"admin.ui_web — веб-панель администрирования.\n\nПорт AdminServices/ServiceAction, AdminAdmins, AdminBackups/Restore/Delete,\nAdminSystem, AdminF2B, AdminScripts, AdminMessages, GroupAdmin/GroupMessages\nиз старого messengers/web.py. Скрипты и пути — из .config пакета\n(вендор scripts_admin/, ADMIN_PASS). Группы — через ctx.api ai_chat\n(файлы и модели — владелец ai_chat). Журнал — через ctx.api admin.journal.\n\"\"\"\n\nROUTES = [\n    (\"POST\", \"/api/admin/admins\", \"admins\", {\"right\": \"manage_admins\",\n     \"desc\": \"Назначить/снять TG-админа {action: add|remove, id}\"}),\n    (\"POST\", \"/api/admin/system\", \"system\", {\"right\": None,\n     \"desc\": \"Перезапуск/выключение {action: restart_bot|restart_machine|shutdown} (права по действию)\"}),\n    (\"GET\", \"/api/admin/scripts\", \"scripts\", {\"right\": \"run_scripts\",\n     \"desc\": \"Список scripts/*.sh\"}),\n    (\"POST\", \"/api/admin/scripts\", \"scripts\", {\"right\": \"run_scripts\",\n     \"desc\": \"Запуск скрипта {script, params?}\"}),\n    (\"GET\", \"/api/admin/scripts/content\", \"script_content\", {\"right\": \"run_scripts\",\n     \"desc\": \"Текст скрипта (?name)\"}),\n    (\"POST\", \"/api/admin/scripts/save\", \"script_save\", {\"right\": \"run_scripts\",\n     \"desc\": \"Создать/сохранить скрипт {name, content} (bash -n проверка)\"}),\n    (\"POST\", \"/api/admin/scripts/delete\", \"script_delete\", {\"right\": \"run_scripts\",\n     \"desc\": \"Удалить скрипт {name}\"}),\n    (\"GET\", \"/api/admin/messages\", \"messages\", {\"right\": \"view_user_messages\",\n     \"desc\": \"Журнал сообщений пользователей (limit/offset/q/user_id/sort/filter_group)\"}),\n    (\"GET\", \"/api/admin/tgmenu\", \"tgmenu\", {\"right\": \"menu_edit\",\n     \"desc\": \"Каталог кнопок /start + раскладка (для редактора)\"}),\n    (\"POST\", \"/api/admin/tgmenu\", \"tgmenu\", {\"right\": \"menu_edit\",\n     \"desc\": \"Сохранить раскладку /start {buttons: [{id, wide, hidden}]}\"}),\n]\n\nSERVICE = [\n    {\"key\": \"system\", \"icon\": \"⚙️\", \"title\": \"Система\", \"right\": \"restart_bot\",\n     \"phase\": 3, \"desc\": \"Перезапуск бота и хоста (бэкапы — карточка «Бэкапы»)\"},\n    {\"key\": \"run_scripts\", \"icon\": \"▶️\", \"title\": \"Скрипты\", \"right\": \"run_scripts\",\n     \"phase\": 3, \"desc\": \"Запуск скриптов обслуживания\"},\n    {\"key\": \"view_user_messages\", \"icon\": \"📨\", \"title\": \"Сообщения пользователей\",\n     \"right\": \"view_user_messages\", \"phase\": 3,\n     \"desc\": \"Журнал сообщений пользователей бота (включая группы)\"},\n    {\"key\": \"tgmenu_edit\", \"icon\": \"🗂️\", \"title\": \"Меню /start\", \"right\": \"menu_edit\",\n     \"phase\": 2, \"desc\": \"Раскладка кнопок Telegram-меню: порядок, ширина, скрытие\"},\n]\n\nPANELS = []\n\n\ndef _check(ctx, sess, right) -> bool:\n    try:\n        return bool(ctx.api.call(\"transport_web.check\", sess, right))\n    except Exception:  # noqa: BLE001\n        return False\n\n\ndef _pkg():\n    from . import config as pkg_config\n    return pkg_config\n\n\ndef _sudo_run(cmd, pwd, timeout=120):\n    import subprocess as _sp\n    r = _sp.run([\"sudo\", \"-S\", \"-p\", \"\", *cmd], input=(pwd or \"\") + \"\\n\",\n                text=True, capture_output=True, timeout=timeout)\n    return r\n\n\ndef handle_api(ctx, config, method, req):\n    import json as _json\n    import os as _os\n    import re as _re\n    import subprocess as _sp\n    import time as _time\n\n    from core.errors import UserError\n\n    pkg = _pkg()\n    sess = req.get(\"session\") or {}\n    uid = sess.get(\"uid\")\n\n    if method == \"admins\":\n        body = req.get(\"body\") or {}\n        action = (body.get(\"action\") or \"\").strip()\n        tid = str(body.get(\"id\") or \"\").strip()\n        if action not in (\"add\", \"remove\"):\n            return 400, {\"ok\": False, \"error\": \"action должен быть add/remove\"}\n        if not tid.isdigit():\n            return 400, {\"ok\": False, \"error\": \"ID должен быть числом\"}\n        # Владелец списка — access (единственный ADMIN_IDS).\n        try:\n            if action == \"add\":\n                changed, msg = ctx.api.call(\"access.add_admin\", tid)\n                return {\"ok\": True, \"changed\": changed, \"msg\": msg}\n            try:\n                cfg_changed, rights_changed = ctx.api.call(\"access.remove_admin\", tid)\n            except LookupError as e:\n                return 400, {\"ok\": False, \"error\": str(e)}\n            return {\"ok\": True, \"cfg_changed\": cfg_changed,\n                    \"rights_changed\": rights_changed,\n                    \"msg\": f\"Администратор {tid} удалён\"\n                           + (\" (требуется перезапуск)\" if cfg_changed else \"\")}\n        except (ValueError, RuntimeError) as e:\n            return 400, {\"ok\": False, \"error\": str(e)}\n        except Exception as e:  # noqa: BLE001\n            return 500, {\"ok\": False, \"error\": str(e)[:400]}\n\n    if method == \"system\":\n        if uid is None:\n            return 401, {\"ok\": False, \"error\": \"Требуется вход\"}\n        body = req.get(\"body\") or {}\n        action = (body.get(\"action\") or \"\").strip()\n        need = {\"restart_bot\": \"restart_bot\", \"restart_machine\": \"restart_machine\",\n                \"shutdown\": \"shutdown_host\"}.get(action)\n        if not need:\n            return 400, {\"ok\": False, \"error\": \"Недопустимое действие\"}\n        if not _check(ctx, sess, need):\n            return 403, {\"ok\": False, \"error\": f\"Нет права {need}\"}\n        pwd = getattr(pkg, \"ADMIN_PASS\", \"\") or \"\"\n        try:\n            if action == \"restart_bot\":\n                try:\n                    unit = (config.get(\"SYSTEMD_SERVICE\", \"\") or \"\").strip() \\\n                        if config else \"\"\n                except Exception:  # noqa: BLE001\n                    unit = \"\"\n                mode = (body.get(\"mode\") or \"auto\").strip()\n                if mode == \"self\" or (mode == \"auto\" and not unit):\n                    # B: самоперезапуск execv (без sudo/systemd; ответ уйдёт\n                    # до рестарта благодаря задержке).\n                    from core import restart as _restart\n                    try:\n                        _restart.restart_self()\n                    except RuntimeError as e:\n                        return 400, {\"ok\": False, \"error\": str(e)}\n                    return {\"ok\": True, \"msg\": \"Перезапускаюсь (execv)\"}\n                if not unit:\n                    return 400, {\"ok\": False,\n                                 \"error\": \"Служба не настроена (SYSTEMD_SERVICE пуст)\"}\n                r = _sp.run([\"sudo\", \"-S\", \"-p\", \"\", \"systemctl\", \"restart\", unit],\n                            input=pwd + \"\\n\", text=True, capture_output=True, timeout=30)\n                if r.returncode != 0:\n                    return 500, {\"ok\": False,\n                                 \"error\": ((r.stderr or r.stdout).strip()\n                                           or f\"exit {r.returncode}\")[:400]}\n                return {\"ok\": True, \"msg\": \"Бот перезапущен\"}\n            if action == \"restart_machine\":\n                _sp.Popen([\"sudo\", \"-S\", \"-p\", \"\", \"shutdown\", \"-r\", \"now\"],\n                          stdin=_sp.PIPE, stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,\n                          text=True).communicate(input=pwd + \"\\n\", timeout=5)\n                return {\"ok\": True, \"msg\": \"Хост перезагружается\"}\n            _sp.Popen([\"sudo\", \"-S\", \"-p\", \"\", \"shutdown\", \"-h\", \"now\"],\n                      stdin=_sp.PIPE, stdout=_sp.DEVNULL, stderr=_sp.DEVNULL,\n                      text=True).communicate(input=pwd + \"\\n\", timeout=5)\n            return {\"ok\": True, \"msg\": \"Хост выключается\"}\n        except Exception as e:  # noqa: BLE001\n            return 500, {\"ok\": False, \"error\": str(e)[:400]}\n\n    def _scripts_dir():\n        # Канонический каталог пользовательских скриптов — services/scripts.\n        return _os.path.join(_os.path.dirname(_os.path.abspath(__file__)),\n                             \"..\", \"services\", \"scripts\")\n\n    if method == \"scripts\":\n        sdir = _scripts_dir()\n        if req.get(\"http_method\") == \"GET\":\n            try:\n                files = _os.listdir(sdir)\n            except Exception:  # noqa: BLE001\n                return {\"scripts\": []}\n            out = []\n            for f in sorted(files):\n                if not f.endswith(\".sh\"):\n                    continue\n                path = _os.path.join(sdir, f)\n                try:\n                    st = _os.stat(path)\n                    out.append({\"name\": f, \"size\": st.st_size,\n                                \"mtime\": _time.strftime(\"%Y-%m-%d %H:%M:%S\",\n                                                       _time.localtime(st.st_mtime))})\n                except Exception:  # noqa: BLE001\n                    out.append({\"name\": f})\n            return {\"scripts\": out}\n        body = req.get(\"body\") or {}\n        name = (body.get(\"script\") or body.get(\"name\") or \"\").strip()\n        params = (body.get(\"params\") or \"\").strip()\n        if not _re.match(r\"^[A-Za-z0-9_.\\-]+\\.sh$\", name):\n            return 400, {\"ok\": False, \"error\": \"Некорректное имя скрипта\"}\n        if params and _re.search(r\"[;&|`$()]\", params):\n            return 400, {\"ok\": False, \"error\": \"Недопустимые символы в параметрах\"}\n        path = _os.path.join(sdir, name)\n        if not _os.path.isfile(path):\n            return 400, {\"ok\": False, \"error\": \"Скрипт не найден\"}\n        pwd = getattr(pkg, \"ADMIN_PASS\", \"\") or \"\"\n        cmd = [\"bash\", path]\n        if params:\n            cmd += params.split()\n        try:\n            r = _sudo_run(cmd, pwd, timeout=120)\n        except Exception as e:  # noqa: BLE001\n            return 500, {\"ok\": False, \"error\": str(e)[:2000]}\n        out = (r.stdout or \"\") + ((\"\\n\" + r.stderr) if r.stderr else \"\")\n        if r.returncode != 0:\n            return 500, {\"ok\": False, \"error\": (out.strip() or f\"exit {r.returncode}\")[:2000]}\n        return {\"ok\": True, \"output\": (out.strip() or \"OK\")[:4000]}\n\n    def _script_name(raw):\n        name = (raw or \"\").strip()\n        if not _re.match(r\"^[A-Za-z0-9_.\\-]+\\.sh$\", name):\n            return \"\"\n        return name\n\n    if method == \"script_content\":\n        name = _script_name((req.get(\"query\") or {}).get(\"name\"))\n        if not name:\n            return 400, {\"ok\": False, \"error\": \"Некорректное имя скрипта\"}\n        path = _os.path.join(_scripts_dir(), name)\n        if not _os.path.isfile(path):\n            return 400, {\"ok\": False, \"error\": \"Скрипт не найден\"}\n        try:\n            with open(path, \"r\", encoding=\"utf-8\", errors=\"replace\") as f:\n                content = f.read()\n        except OSError as e:\n            return 500, {\"ok\": False, \"error\": str(e)[:200]}\n        if len(content) > 262144:\n            return 400, {\"ok\": False, \"error\": \"Скрипт больше 256 КБ\"}\n        return {\"ok\": True, \"name\": name, \"content\": content}\n\n    if method == \"script_save\":\n        body = req.get(\"body\") or {}\n        name = _script_name(body.get(\"name\"))\n        content = body.get(\"content\") or \"\"\n        if not name:\n            return 400, {\"ok\": False, \"error\": \"Некорректное имя скрипта (латиница, .sh)\"}\n        if not isinstance(content, str) or not content.strip():\n            return 400, {\"ok\": False, \"error\": \"Пустое содержимое\"}\n        if len(content) > 262144:\n            return 400, {\"ok\": False, \"error\": \"Скрипт больше 256 КБ\"}\n        if not content.lstrip().startswith(\"#!\"):\n            content = \"#!/usr/bin/env bash\\n\" + content\n        path = _os.path.join(_scripts_dir(), name)\n        tmp = path + \".tmp\"\n        try:\n            with open(tmp, \"w\", encoding=\"utf-8\") as f:\n                f.write(content if content.endswith(\"\\n\") else content + \"\\n\")\n            # синтаксис — до замены боевого файла\n            r = _sp.run([\"bash\", \"-n\", tmp], text=True,\n                        capture_output=True, timeout=15)\n            if r.returncode != 0:\n                _os.remove(tmp)\n                return 400, {\"ok\": False,\n                             \"error\": \"Синтаксис: %s\" % ((r.stderr or \"\").strip()[:2000])}\n            _os.replace(tmp, path)\n            _os.chmod(path, 0o755)\n        except OSError as e:\n            try:\n                _os.remove(tmp)\n            except OSError:\n                pass\n            return 500, {\"ok\": False, \"error\": str(e)[:200]}\n        return {\"ok\": True, \"name\": name, \"size\": len(content)}\n\n    if method == \"script_delete\":\n        body = req.get(\"body\") or {}\n        name = _script_name(body.get(\"name\"))\n        if not name:\n            return 400, {\"ok\": False, \"error\": \"Некорректное имя скрипта\"}\n        path = _os.path.join(_scripts_dir(), name)\n        if not _os.path.isfile(path):\n            return 400, {\"ok\": False, \"error\": \"Скрипт не найден\"}\n        try:\n            _os.remove(path)\n        except OSError as e:\n            return 500, {\"ok\": False, \"error\": str(e)[:200]}\n        return {\"ok\": True, \"name\": name}\n\n    if method == \"messages\":\n        query = req.get(\"query\") or {}\n        try:\n            limit = int(query.get(\"limit\", \"50\") or 50)\n        except (ValueError, TypeError):\n            limit = 50\n        limit = max(1, min(200, limit))\n        try:\n            offset = int(query.get(\"offset\", \"0\") or 0)\n        except (ValueError, TypeError):\n            offset = 0\n        offset = max(0, offset)\n        q = (query.get(\"q\", \"\") or \"\").strip().lower()\n        q_user = (query.get(\"user_id\", \"\") or \"\").strip()\n        sort_by = (query.get(\"sort\", \"\") or \"\").strip().lower()\n        filter_group = (query.get(\"filter_group\", \"\") or \"\").strip().lower()\n        try:\n            msgs = ctx.api.call(\"admin.journal\", 2000)\n        except Exception as e:  # noqa: BLE001\n            return 500, {\"error\": str(e)[:400]}\n        enriched = []\n        seen = set()\n        for m in msgs:\n            key = (m.get(\"text\") or \"\", str(m.get(\"user_id\") or \"\"),\n                   str(m.get(\"chat_id\") or \"\"), (m.get(\"time\") or \"\")[:19])\n            if key in seen:\n                continue\n            seen.add(key)\n            u = str(m.get(\"user_id\") or \"\")\n            name_cached = m.get(\"name\") or \"\"\n            try:\n                disp = ctx.api.call(\"tg_names.resolve\", u) or name_cached\n            except Exception:  # noqa: BLE001\n                disp = name_cached\n            ct = m.get(\"chat_type\") or \"private\"\n            is_grp = bool(m.get(\"is_group\")) or ct in (\"group\", \"supergroup\")\n            enriched.append({\n                \"user_id\": u, \"name\": disp, \"name_cached\": name_cached,\n                \"text\": m.get(\"text\") or \"\", \"time\": m.get(\"time\") or \"\",\n                \"time_short\": (m.get(\"time\") or \"\")[:19].replace(\"T\", \" \"),\n                \"chat_id\": str(m.get(\"chat_id\") or \"\"), \"chat_type\": ct,\n                \"chat_title\": m.get(\"chat_title\") or \"\", \"is_group\": is_grp,\n            })\n        if q_user:\n            enriched = [x for x in enriched if x[\"user_id\"] == q_user]\n        if q:\n            enriched = [x for x in enriched if q in x[\"text\"].lower()\n                        or q in x[\"name\"].lower() or q in x[\"user_id\"]\n                        or q in (x.get(\"chat_title\") or \"\").lower()\n                        or q in (x.get(\"chat_id\") or \"\")]\n        if filter_group == \"group\":\n            enriched = [x for x in enriched if x[\"is_group\"]]\n        elif filter_group == \"private\":\n            enriched = [x for x in enriched if not x[\"is_group\"]]\n        if sort_by == \"group\":\n            enriched.sort(key=lambda x: x[\"time\"], reverse=True)\n            enriched.sort(key=lambda x: x[\"is_group\"], reverse=True)\n        elif sort_by == \"chat\":\n            enriched.sort(key=lambda x: (x[\"chat_title\"] or x[\"chat_id\"] or \"\").lower())\n        else:\n            enriched.sort(key=lambda x: x[\"time\"], reverse=True)\n        total = len(enriched)\n        users_map = {}\n        for x in enriched:\n            u = x[\"user_id\"]\n            if u not in users_map:\n                users_map[u] = {\"user_id\": u, \"name\": x[\"name\"], \"count\": 0}\n            users_map[u][\"count\"] += 1\n        users = sorted(users_map.values(), key=lambda u: u[\"count\"], reverse=True)\n        return {\"total\": total, \"messages\": enriched[offset:offset + limit],\n                \"users\": users, \"limit\": limit, \"offset\": offset}\n\n    if method == \"tgmenu\":\n        from . import tgmenu as _tgmenu\n        catalog = _tgmenu.collect_buttons(ctx.loaded_modules)\n        if req.get(\"http_method\") == \"GET\":\n            layout = _tgmenu.load_layout(ctx.data_dir, catalog)\n            return {\"ok\": True, \"catalog\": catalog, \"layout\": layout}\n        body = req.get(\"body\") or {}\n        ok, error = _tgmenu.save_layout(\n            ctx.data_dir, catalog, body.get(\"buttons\"))\n        if not ok:\n            return 400, {\"ok\": False, \"error\": error}\n        return {\"ok\": True}\n\n    raise UserError(\"неизвестный метод: %s\" % method)\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"admin.ui_web — веб-панель администрирования.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Порт AdminServices/ServiceAction, AdminAdmins, AdminBackups/Restore/Delete,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">AdminSystem, AdminF2B, AdminScripts, AdminMessages, GroupAdmin/GroupMessages</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">из старого messengers/web.py. Скрипты и пути — из .config пакета</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(вендор scripts_admin/, ADMIN_PASS). Группы — через ctx.api ai_chat</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(файлы и модели — владелец ai_chat). Журнал — через ctx.api admin.journal.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ROUTES</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/admins\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"admins\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"manage_admins\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Назначить/снять TG-админа </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{action</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> add|remove, id}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/system\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"system\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Перезапуск/выключение </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{action</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> restart_bot|restart_machine|shutdown}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (права по действию)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"run_scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Список scripts/*.sh\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"run_scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Запуск скрипта {script, params?}\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/scripts/content\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"script_content\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"run_scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Текст скрипта (?name)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/scripts/save\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"script_save\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"run_scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Создать/сохранить скрипт {name, content} (bash -n проверка)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/scripts/delete\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"script_delete\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"run_scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Удалить скрипт </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{name}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"view_user_messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Журнал сообщений пользователей (limit/offset/q/user_id/sort/filter_group)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/tgmenu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"tgmenu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"menu_edit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Каталог кнопок /start + раскладка (для редактора)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/tgmenu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"tgmenu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"menu_edit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Сохранить раскладку /start </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{buttons</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> [{id, wide, hidden}]}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">SERVICE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"system\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"icon\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"⚙️\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Система\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart_bot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"phase\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Перезапуск бота и хоста (бэкапы — карточка «Бэкапы»)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"run_scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"icon\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"▶️\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Скрипты\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"run_scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"phase\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Запуск скриптов обслуживания\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"view_user_messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"icon\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📨\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Сообщения пользователей\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"view_user_messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"phase\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Журнал сообщений пользователей бота (включая группы)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"tgmenu_edit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"icon\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🗂️\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Меню /start\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"menu_edit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"phase\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Раскладка кнопок Telegram-меню: порядок, ширина, скрытие\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">PANELS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _check</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx, sess, right) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"transport_web.check\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, sess, right))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _pkg</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pkg_config</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pkg_config</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _sudo_run</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(cmd, pwd, timeout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">120</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> subprocess </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _sp</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    r </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _sp.run([</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"sudo\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-S\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-p\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">cmd], </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">input</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(pwd </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">capture_output</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">timeout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">timeout)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> handle_api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx, config, method, req):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _json</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _os</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> subprocess </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _sp</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> time </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _time</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.errors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    pkg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _pkg()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    sess </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"session\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    uid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sess.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"uid\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"admins\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        action </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"action\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        tid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> action </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"add\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"remove\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"action должен быть add/remove\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tid.isdigit():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ID должен быть числом\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Владелец списка — access (единственный ADMIN_IDS).</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> action </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"add\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                changed, msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"access.add_admin\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, tid)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"changed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: changed, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: msg}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                cfg_changed, rights_changed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"access.remove_admin\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, tid)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> LookupError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cfg_changed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: cfg_changed,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"rights_changed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: rights_changed,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"msg\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Администратор </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">tid</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> удалён\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                           +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\" (требуется перезапуск)\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cfg_changed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">RuntimeError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"system\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> uid </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 401</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Требуется вход\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        action </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"action\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        need </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart_bot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart_bot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart_machine\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart_machine\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"shutdown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"shutdown_host\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}.get(action)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> need:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Недопустимое действие\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _check(ctx, sess, need):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 403</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Нет права </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">need</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        pwd </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(pkg, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ADMIN_PASS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> action </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"restart_bot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    unit </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (config.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SYSTEMD_SERVICE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip() \\</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    unit </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                mode </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mode\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"auto\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mode </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"self\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (mode </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"auto\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> and</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> unit):</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                    # B: самоперезапуск execv (без sudo/systemd; ответ уйдёт</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                    # до рестарта благодаря задержке).</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> restart </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _restart</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        _restart.restart_self()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> RuntimeError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Перезапускаюсь (execv)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> unit:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                                 \"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Служба не настроена (SYSTEMD_SERVICE пуст)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                r </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _sp.run([</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"sudo\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-S\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-p\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"systemctl\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, unit],</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                            input</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">pwd </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">capture_output</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">timeout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">30</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.returncode </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                                 \"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: ((r.stderr </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.stdout).strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                                           or</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"exit </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.returncode</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Бот перезапущен\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> action </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"restart_machine\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                _sp.Popen([</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"sudo\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-S\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-p\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"shutdown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-r\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"now\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                          stdin</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">_sp.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">PIPE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">stdout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">_sp.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">DEVNULL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">stderr</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">_sp.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">DEVNULL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                          text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).communicate(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">input</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">pwd </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">timeout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Хост перезагружается\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            _sp.Popen([</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"sudo\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-S\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-p\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"shutdown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-h\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"now\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                      stdin</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">_sp.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">PIPE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">stdout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">_sp.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">DEVNULL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">stderr</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">_sp.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">DEVNULL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                      text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).communicate(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">input</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">pwd </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">timeout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Хост выключается\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _scripts_dir</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">():</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Канонический каталог пользовательских скриптов — services/scripts.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _os.path.join(_os.path.dirname(_os.path.abspath(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__file__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                             \"..\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"services\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        sdir </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _scripts_dir()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"http_method\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                files </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _os.listdir(sdir)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: []}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(files):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f.endswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".sh\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                path </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _os.path.join(sdir, f)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    st </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _os.stat(path)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    out.append({</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: f, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"size\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: st.st_size,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                                \"mtime\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _time.strftime(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"%Y-%m-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> %H:%M:%S\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                                                       _time.localtime(st.st_mtime))})</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    out.append({</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: f})</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: out}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"script\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        params </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"params\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re.match(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">^[A-Za-z0-9_.</span><span style=\"color:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold\">\\-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold\">\\.</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">sh</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">$</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, name):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Некорректное имя скрипта\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> params </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re.search(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">[;&#x26;|`$()]</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, params):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Недопустимые символы в параметрах\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        path </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _os.path.join(sdir, name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _os.path.isfile(path):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Скрипт не найден\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        pwd </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> getattr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(pkg, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ADMIN_PASS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        cmd </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"bash\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, path]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> params:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            cmd </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> params.split()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            r </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _sudo_run(cmd, pwd, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">timeout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">120</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2000</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        out </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (r.stdout </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.stderr) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.stderr </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.returncode </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: (out.strip() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"exit </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.returncode</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2000</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"output\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: (out.strip() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"OK\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">4000</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _script_name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(raw):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (raw </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re.match(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">^[A-Za-z0-9_.</span><span style=\"color:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold\">\\-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold\">\\.</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">sh</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">$</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, name):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"script_content\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _script_name((req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"query\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}).get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Некорректное имя скрипта\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        path </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _os.path.join(_scripts_dir(), name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _os.path.isfile(path):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Скрипт не найден\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(path, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"r\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">errors</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"replace\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                content </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f.read()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> OSError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">200</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(content) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 262144</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Скрипт больше 256 КБ\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: name, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"content\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: content}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"script_save\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _script_name(body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        content </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"content\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Некорректное имя скрипта (латиница, .sh)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(content, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> content.strip():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Пустое содержимое\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(content) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 262144</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Скрипт больше 256 КБ\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> content.lstrip().startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            content </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"#!/usr/bin/env bash</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> content</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        path </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _os.path.join(_scripts_dir(), name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        tmp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> path </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \".tmp\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(tmp, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"w\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                f.write(content </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> content.endswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> content </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # синтаксис — до замены боевого файла</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            r </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _sp.run([</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"bash\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-n\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, tmp], </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                        capture_output</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">timeout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">15</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.returncode </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                _os.remove(tmp)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                             \"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Синтаксис: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((r.stderr </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2000</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            _os.replace(tmp, path)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            _os.chmod(path, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">0o</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">755</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> OSError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                _os.remove(tmp)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> OSError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                pass</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">200</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: name, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"size\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(content)}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"script_delete\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _script_name(body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Некорректное имя скрипта\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        path </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _os.path.join(_scripts_dir(), name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _os.path.isfile(path):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Скрипт не найден\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            _os.remove(path)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> OSError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">200</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: name}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        query </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"query\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            limit </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(query.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"limit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"50\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 50</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TypeError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            limit </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 50</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        limit </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> max</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">min</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">200</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, limit))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(query.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"offset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"0\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TypeError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> max</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, offset)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        q </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (query.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"q\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip().lower()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        q_user </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (query.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"user_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        sort_by </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (query.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"sort\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip().lower()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        filter_group </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (query.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"filter_group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip().lower()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            msgs </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"admin.journal\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2000</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        enriched </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        seen </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgs:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"text\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"user_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                   str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), (m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"time\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">19</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> seen:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            seen.add(key)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            u </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"user_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            name_cached </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                disp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"tg_names.resolve\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, u) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name_cached</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                disp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name_cached</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ct </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat_type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"private\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            is_grp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"is_group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ct </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"supergroup\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            enriched.append({</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"user_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: u, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: disp, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_cached\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: name_cached,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"text\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"text\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"time\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"time\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"time_short\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: (m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"time\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">19</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].replace(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"T\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\" \"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"chat_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat_type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: ct,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"chat_title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat_title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"is_group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: is_grp,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            })</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> q_user:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            enriched </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [x </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> enriched </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"user_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> q_user]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> q:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            enriched </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [x </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> enriched </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> q </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"text\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].lower()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> q </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].lower() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> q </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"user_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> q </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (x.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat_title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).lower()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> q </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (x.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> filter_group </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            enriched </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [x </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> enriched </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"is_group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> filter_group </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"private\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            enriched </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [x </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> enriched </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"is_group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sort_by </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            enriched.sort(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">key</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=lambda</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x: x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"time\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">reverse</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            enriched.sort(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">key</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=lambda</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x: x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"is_group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">reverse</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sort_by </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"chat\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            enriched.sort(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">key</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=lambda</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x: (x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat_title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).lower())</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            enriched.sort(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">key</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=lambda</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x: x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"time\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">reverse</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        total </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(enriched)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        users_map </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> enriched:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            u </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"user_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> u </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> users_map:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                users_map[u] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"user_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: u, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            users_map[u][</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        users </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(users_map.values(), </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">key</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=lambda</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> u: u[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">reverse</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: total, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: enriched[offset:offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> limit],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"users\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: users, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"limit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: limit, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"offset\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: offset}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"tgmenu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tgmenu </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _tgmenu</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        catalog </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _tgmenu.collect_buttons(ctx.loaded_modules)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"http_method\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            layout </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _tgmenu.load_layout(ctx.data_dir, catalog)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"catalog\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: catalog, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"layout\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: layout}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ok, error </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _tgmenu.save_layout(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ctx.data_dir, catalog, body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"buttons\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ok:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: error}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"неизвестный метод: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method)</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "web/view.html",
          "size": 5082,
          "content": "<!-- Модульные вьюхи admin. -->\n<section id=\"view-messages\" class=\"hidden\">\n  <h2>Сообщения пользователей</h2>\n  <p class=\"muted\">Журнал из <code>cache/user_messages.json</code> (30 дней). Требует право <code>view_user_messages</code>.</p>\n  <div class=\"msg-toolbar\">\n    <input id=\"msg-q\" type=\"search\" placeholder=\"Поиск по тексту, имени, ID…\" autocomplete=\"off\">\n    <select id=\"msg-user\"></select>\n    <select id=\"msg-group-filter\"><option value=\"\">Все чаты</option><option value=\"group\">Только группы 👥</option><option value=\"private\">Только личка 👤</option></select>\n    <select id=\"msg-sort\"><option value=\"\">Сорт: время ↓</option><option value=\"group\">Сорт: группы первыми</option><option value=\"chat\">Сорт: по чату</option></select>\n    <select id=\"msg-limit\"><option value=\"25\">25</option><option value=\"50\" selected>50</option><option value=\"100\">100</option><option value=\"200\">200</option></select>\n    <button id=\"msg-refresh\" class=\"btn-mini\">Обновить</button>\n  </div>\n  <p class=\"muted\" id=\"msg-info\"></p>\n  <div class=\"table-wrap\">\n    <table id=\"msg-table\">\n      <thead><tr><th style=\"min-width:130px\" class=\"sortable\" data-key=\"time\">Время</th><th class=\"sortable\" data-key=\"group\">Пользователь / Чат</th><th>Сообщение</th></tr></thead>\n      <tbody></tbody>\n    </table>\n  </div>\n  <div class=\"msg-pager\">\n    <button id=\"msg-prev\" class=\"btn-mini\">← Назад</button>\n    <span id=\"msg-page\" class=\"muted\"></span>\n    <button id=\"msg-next\" class=\"btn-mini\">Далее →</button>\n  </div>\n</section>\n<section id=\"view-tgmenu\" class=\"hidden\">\n  <h2>Меню /start</h2>\n  <p class=\"muted\">Как кнопки выглядят в Telegram. Кнопки заявляют сами модули — здесь только раскладка.</p>\n  <div class=\"toolbar\" style=\"margin-bottom:12px\">\n    <button id=\"tgmenu-edit-toggle\" class=\"btn-mini hidden\" onclick=\"toggleTgMenuEdit()\">Редактировать</button>\n  </div>\n  <div class=\"tg-preview\" id=\"tgmenu-preview\"></div>\n  <div id=\"tgmenu-hidden\" class=\"hidden\"></div>\n</section>\n<section id=\"view-system\" class=\"hidden\">\n  <h2>Система</h2>\n  <p class=\"muted\">Бэкапы переехали в карточку <b>«Бэкапы»</b> (право <code>backups_manage</code>). Управление ботом/хостом — <code>restart_bot</code>/<code>restart_machine</code>/<code>shutdown_host</code>.</p>\n  <div class=\"grid\">\n    <article class=\"card\">\n      <h3>Управление</h3>\n      <div class=\"toolbar\">\n        <button id=\"sys-restart-bot\" class=\"btn-mini\">Перезапустить бота</button>\n        <button id=\"sys-restart-machine\" class=\"btn-mini danger\">Перезагрузить хост</button>\n        <button id=\"sys-shutdown\" class=\"btn-mini danger\">Выключить хост</button>\n      </div>\n      <p class=\"muted\" id=\"sys-msg\" style=\"min-height:18px;margin-top:10px\"></p>\n      <p class=\"muted\" style=\"font-size:11px\">Перезапуск/выключение выполняется через <code>sudo</code> с паролем из <code>ADMIN_PASS</code>.</p>\n    </article>\n  </div>\n</section>\n<section id=\"view-scripts\" class=\"hidden\">\n  <h2>Скрипты</h2>\n  <p class=\"muted\">Каталог <code>services/scripts/*.sh</code> (как в Telegram: <code>run_scripts</code>). Параметры проверяются на <code>[;&amp;|`$()]</code>.</p>\n  <div class=\"toolbar\" style=\"margin-bottom:8px\">\n    <button class=\"btn-mini\" onclick=\"scrNew()\">➕ Новый скрипт</button>\n  </div>\n  <div class=\"table-wrap\"><table id=\"scr-table\"><thead><tr><th>Скрипт</th><th>Размер</th><th>Изменён</th><th>Действие</th></tr></thead><tbody></tbody></table></div>\n  <p class=\"muted\" id=\"scr-info\"></p>\n  <div class=\"card hidden\" id=\"scr-edit-card\" style=\"margin-top:12px\">\n    <h3 id=\"scr-edit-title\">Новый скрипт</h3>\n    <div class=\"form-grid\">\n      <input id=\"scr-name\" placeholder=\"имя_файла.sh (латиница)\" style=\"grid-column:1/-1\">\n      <textarea id=\"scr-text\" rows=\"14\" class=\"mono\" placeholder=\"#!/usr/bin/env bash&#10;...\" style=\"font-size:12px\"></textarea>\n    </div>\n    <div class=\"toolbar\">\n      <button class=\"btn-mini\" onclick=\"scrSave(this)\">Сохранить</button>\n      <button class=\"btn-mini\" onclick=\"scrEditHide()\">Отмена</button>\n      <span class=\"muted\" id=\"scr-edit-msg\"></span>\n    </div>\n  </div>\n  <div class=\"card\" style=\"margin-top:12px\">\n    <h3>Вывод</h3>\n    <div class=\"toolbar\" style=\"margin-bottom:8px\">\n      <input id=\"scr-params\" placeholder=\"параметры (опционально)\" style=\"flex:1;min-width:160px\">\n      <span class=\"muted\" style=\"font-size:12px;align-self:center\" id=\"scr-chosen\">не выбран</span>\n    </div>\n    <pre id=\"scr-out\" class=\"mono\" style=\"max-height:320px\"></pre>\n  </div>\n</section>\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">&#x3C;!-- Модульные вьюхи admin. --></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">section</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"view-messages\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Сообщения пользователей&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Журнал из &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>cache/user_messages.json&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">> (30 дней). Требует право &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>view_user_messages&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>.&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg-toolbar\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">input</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg-q\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> type</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"search\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> placeholder</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Поиск по тексту, имени, ID…\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> autocomplete</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"off\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">select</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg-user\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">select</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">select</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg-group-filter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> value</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Все чаты&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> value</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Только группы 👥&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> value</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"private\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Только личка 👤&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">select</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">select</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg-sort\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> value</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Сорт: время ↓&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> value</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Сорт: группы первыми&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> value</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"chat\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Сорт: по чату&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">select</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">select</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg-limit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> value</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"25\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>25&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> value</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"50\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> selected</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>50&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> value</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"100\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>100&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> value</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"200\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>200&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">option</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">select</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg-refresh\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Обновить&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg-info\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"table-wrap\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">table</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg-table\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">thead</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">tr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"min-width:130px\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"sortable\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> data-key</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"time\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Время&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"sortable\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> data-key</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Пользователь / Чат&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Сообщение&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">tr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">thead</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">tbody</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">tbody</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">table</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg-pager\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg-prev\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>← Назад&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg-page\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"msg-next\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Далее →&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">section</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">section</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"view-tgmenu\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Меню /start&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Как кнопки выглядят в Telegram. Кнопки заявляют сами модули — здесь только раскладка.&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toolbar\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"margin-bottom:12px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"tgmenu-edit-toggle\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini hidden\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> onclick</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">toggleTgMenuEdit</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">()\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Редактировать&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"tg-preview\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"tgmenu-preview\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"tgmenu-hidden\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">section</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">section</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"view-system\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Система&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Бэкапы переехали в карточку &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">b</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>«Бэкапы»&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">b</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">> (право &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>backups_manage&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>). Управление ботом/хостом — &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>restart_bot&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>/&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>restart_machine&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>/&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>shutdown_host&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>.&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"grid\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">article</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"card\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Управление&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toolbar\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"sys-restart-bot\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Перезапустить бота&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"sys-restart-machine\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini danger\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Перезагрузить хост&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"sys-shutdown\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini danger\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Выключить хост&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"sys-msg\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"min-height:18px;margin-top:10px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"font-size:11px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Перезапуск/выключение выполняется через &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>sudo&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">> с паролем из &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>ADMIN_PASS&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>.&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">article</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">section</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">section</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"view-scripts\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Скрипты&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Каталог &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>services/scripts/*.sh&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">> (как в Telegram: &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>run_scripts&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>). Параметры проверяются на &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>[;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">&#x26;amp;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">|`$()]&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>.&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toolbar\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"margin-bottom:8px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> onclick</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">scrNew</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">()\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>➕ Новый скрипт&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"table-wrap\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">table</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scr-table\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">thead</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">tr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Скрипт&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Размер&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Изменён&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Действие&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">tr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">thead</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">tbody</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">tbody</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">table</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scr-info\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"card hidden\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scr-edit-card\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"margin-top:12px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scr-edit-title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Новый скрипт&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"form-grid\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">input</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scr-name\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> placeholder</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"имя_файла.sh (латиница)\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"grid-column:1/-1\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">textarea</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scr-text\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> rows</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"14\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> placeholder</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#!/usr/bin/env bash</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">&#x26;#10;</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">...\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"font-size:12px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">textarea</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toolbar\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> onclick</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">scrSave</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">this</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Сохранить&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> onclick</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">scrEditHide</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">()\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Отмена&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scr-edit-msg\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"card\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"margin-top:12px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Вывод&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toolbar\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"margin-bottom:8px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">input</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scr-params\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> placeholder</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"параметры (опционально)\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"flex:1;min-width:160px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"font-size:12px;align-self:center\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scr-chosen\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>не выбран&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">pre</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scr-out\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"max-height:320px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">pre</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">section</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "web/view.js",
          "size": 21004,
          "content": "/* admin — модульные вьюхи (modules/admin/web/view.js). */\n/* ---------------- сообщения пользователей (view_user_messages) -------- */\n\nconst msgState = { q: \"\", userId: \"\", limit: 50, offset: 0, total: 0, users: [], sort: \"\", filterGroup: \"\" };\nlet msgTimer = null;\n\nasync function loadMessages() {\n  if (!state.me || !(state.me.is_admin || state.me.rights.view_user_messages)) {\n    showToast(\"Нет права view_user_messages\");\n    showView(\"dashboard\");\n    return;\n  }\n  const params = new URLSearchParams({ limit: String(msgState.limit), offset: String(msgState.offset) });\n  if (msgState.q) params.set(\"q\", msgState.q);\n  if (msgState.userId) params.set(\"user_id\", msgState.userId);\n  if (msgState.sort) params.set(\"sort\", msgState.sort);\n  if (msgState.filterGroup) params.set(\"filter_group\", msgState.filterGroup);\n  const r = await api(\"/api/admin/messages?\" + params.toString());\n  const tbody = $(\"#msg-table tbody\");\n  const info = $(\"#msg-info\");\n  if (r.status === 401 || r.status === 403) {\n    tbody.innerHTML = `<tr><td colspan=\"3\" class=\"muted\">Нет доступа (нужно view_user_messages)</td></tr>`;\n    if (info) info.textContent = \"\";\n    return;\n  }\n  if (!r.ok) {\n    tbody.innerHTML = `<tr><td colspan=\"3\" class=\"muted\">Ошибка: ${esc(r.data.error || \"\")}</td></tr>`;\n    return;\n  }\n  msgState.total = r.data.total || 0;\n  msgState.users = r.data.users || [];\n  renderMsgUsers();\n  const msgs = r.data.messages || [];\n  if (!msgs.length) {\n    tbody.innerHTML = `<tr><td colspan=\"3\" class=\"muted\">сообщений не найдено — поправите поиск/фильтр</td></tr>`;\n  } else {\n    tbody.innerHTML = msgs.map((m) => {\n      const chatBadge = m.is_group ? `<br><span class=\"pill ns-telegram\" style=\"font-size:10px\">👥 ${esc(m.chat_title||\"группа\")} <span class=\"mono\">${esc(m.chat_id)}</span></span>` : `<br><span class=\"pill\" style=\"font-size:10px\">👤 личка</span>`;\n      return `\n      <tr>\n        <td class=\"mono muted\" style=\"white-space:nowrap\">${esc(m.time_short)}</td>\n        <td><span class=\"pill off mono\">${esc(m.user_id)}</span><br><span class=\"muted\">${esc(m.name)}</span>${chatBadge}</td>\n        <td style=\"white-space:pre-wrap;word-break:break-word;max-width:520px\">${esc(m.text)}</td>\n      </tr>`;\n    }).join(\"\");\n  }\n  const pages = Math.max(1, Math.ceil(msgState.total / msgState.limit));\n  const cur = Math.floor(msgState.offset / msgState.limit) + 1;\n  $(\"#msg-page\").textContent = `${msgState.total} сообщ. · стр. ${cur} / ${pages}`;\n  $(\"#msg-prev\").disabled = msgState.offset === 0;\n  $(\"#msg-next\").disabled = msgState.offset + msgState.limit >= msgState.total;\n  if (info) info.textContent = msgState.q || msgState.userId ? `фильтр: ${msgState.q ? \"поиск «\" + msgState.q + \"» \" : \"\"}${msgState.userId ? \"пользователь \" + msgState.userId : \"\"}` : \"\";\n}\n\nfunction renderMsgUsers() {\n  const sel = $(\"#msg-user\");\n  if (!sel) return;\n  const prev = msgState.userId;\n  sel.innerHTML = `<option value=\"\">Все пользователи</option>` + msgState.users.map((u) =>\n    `<option value=\"${esc(u.user_id)}\">${esc(u.name || u.user_id)} (${u.user_id}) · ${u.count}</option>`).join(\"\");\n  sel.value = prev;\n  if (sel.value !== prev) msgState.userId = \"\";\n}\n\n$(\"#msg-q\")?.addEventListener(\"input\", (e) => {\n  clearTimeout(msgTimer);\n  msgTimer = setTimeout(() => { msgState.q = e.target.value.trim(); msgState.offset = 0; loadMessages(); }, 350);\n});\n$(\"#msg-user\")?.addEventListener(\"change\", (e) => { msgState.userId = e.target.value; msgState.offset = 0; loadMessages(); });\n$(\"#msg-limit\")?.addEventListener(\"change\", (e) => { msgState.limit = parseInt(e.target.value,10) || 50; msgState.offset = 0; loadMessages(); });\n$(\"#msg-group-filter\")?.addEventListener(\"change\", (e) => { msgState.filterGroup = e.target.value; msgState.offset = 0; loadMessages(); });\n$(\"#msg-sort\")?.addEventListener(\"change\", (e) => { msgState.sort = e.target.value; msgState.offset = 0; loadMessages(); });\n$(\"#msg-refresh\")?.addEventListener(\"click\", () => loadMessages());\n$(\"#msg-prev\")?.addEventListener(\"click\", () => { if (msgState.offset > 0) { msgState.offset = Math.max(0, msgState.offset - msgState.limit); loadMessages(); } });\n$(\"#msg-next\")?.addEventListener(\"click\", () => { if (msgState.offset + msgState.limit < msgState.total) { msgState.offset += msgState.limit; loadMessages(); } });\n\nlet tgMenuEditMode = false;\nlet tgMenuCatalog = [];\nconst TG_WIDTHS = [\"auto\", \"1/4\", \"2/4\", \"3/4\", \"1/3\", \"2/3\", \"full\"];\nconst TG_SLOTS = { \"1/4\": 0.25, \"2/4\": 0.5, \"3/4\": 0.75, \"1/3\": 1 / 3, \"2/3\": 2 / 3 };\n\nfunction tgWidthLabel(w) {\n  return { auto: \"авто\", \"1/4\": \"1/4\", \"2/4\": \"2/4\", \"3/4\": \"3/4\",\n           \"1/3\": \"1/3\", \"2/3\": \"2/3\", full: \"вся строка\" }[w] || w;\n}\n\nasync function renderTgMenuView() {\n  const prev = $(\"#tgmenu-preview\");\n  if (!prev) return;\n  let d = null;\n  try {\n    const r = await api(\"/api/admin/tgmenu\");\n    if (r.status === 403) { showToast(\"Нет права menu_edit\"); showView(\"dashboard\"); return; }\n    if (r.ok) d = r.data;\n  } catch { return; }\n  if (!d || !d.catalog) return;\n  tgMenuCatalog = d.catalog;\n  const cfg = {};\n  for (const e of (d.layout || [])) cfg[e.id] = e;\n  const btns = d.catalog.map((b) => {\n    const e = cfg[b.id] || {};\n    return { ...b, width: e.width || \"auto\", hidden: !!e.hidden,\n             title: e.title || b.title, defTitle: b.title };\n  });\n  const tgl = $(\"#tgmenu-edit-toggle\");\n  if (tgl) tgl.classList.remove(\"hidden\");\n  renderTgPreview(btns);\n}\n\nfunction tgById(id) {\n  return (tgMenuCatalog || []).find((b) => b.id === id) || {};\n}\n\nfunction renderTgPreview(btns) {\n  const prev = $(\"#tgmenu-preview\");\n  const strip = $(\"#tgmenu-hidden\");\n  prev.innerHTML = \"\";\n  if (strip) strip.innerHTML = \"\";\n  // жадная упаковка как в движке: full — своя строка, остальные добирают ряд.\n  let row = null, fill = 0;\n  const flush = () => { row = null; fill = 0; };\n  const newRow = () => {\n    row = document.createElement(\"div\");\n    row.className = \"tg-row\";\n    prev.appendChild(row);\n    fill = 0;\n  };\n  for (const b of btns) {\n    if (b.hidden) {\n      if (strip && tgMenuEditMode) {\n        const s = document.createElement(\"div\");\n        s.className = \"mon-hidden-row\";\n        s.dataset.id = b.id;\n        s.dataset.width = b.width || \"auto\";\n        if (b.title && b.title !== (tgById(b.id).title || \"\")) s.dataset.title = b.title;\n        s.innerHTML = `<b></b> <span class=\"muted\"></span> <button class=\"btn-mini\">показать</button>`;\n        s.querySelector(\"b\").textContent = b.title;\n        s.querySelector(\"span\").textContent = b.id + (tgById(b.id).module ? \" · \" + tgById(b.id).module : \"\");\n        s.querySelector(\"button\").onclick = () => tgMenuShow(b.id);\n        strip.appendChild(s);\n      }\n      continue;\n    }\n    const w = b.width || \"auto\";\n    const need = w === \"full\" ? 1 : (TG_SLOTS[w] || 0.5);\n    if (w === \"full\" || !row || fill + need > 1 + 1e-9) {\n      if (row && row.children.length === 0) prev.removeChild(row);\n      newRow();\n      if (w === \"full\") {\n        row.appendChild(tgButtonEl(b, 1));\n        flush();\n        continue;\n      }\n    }\n    row.appendChild(tgButtonEl(b, need));\n    fill += need;\n  }\n  if (row && row.children.length === 0) prev.removeChild(row);\n  if (strip) strip.classList.toggle(\"hidden\", !tgMenuEditMode);\n}\n\nfunction tgButtonEl(b, need) {\n  const el = document.createElement(\"div\");\n  el.className = \"tg-btn\";\n  el.dataset.id = b.id;\n  el.style.flex = need >= 1 ? \"1 1 100%\" : `1 1 ${(need * 100).toFixed(1)}%`;\n  el.innerHTML = `<span></span>`;\n  el.querySelector(\"span\").textContent = b.title;\n  if (tgMenuEditMode) {\n    const cat = tgById(b.id);\n    const custom = (b.title !== (b.defTitle || cat.title)) ? b.title : \"\";\n    const ctl = document.createElement(\"div\");\n    ctl.className = \"mon-ctl\";\n    const wOpt = TG_WIDTHS.map((v) =>\n      `<option value=\"${v}\" ${(b.width || \"auto\") === v ? \"selected\" : \"\"}>${tgWidthLabel(v)}</option>`).join(\"\");\n    ctl.innerHTML = `\n      <span class=\"mon-mod\">${esc(cat.module || \"\")}</span>\n      <span class=\"mon-mod\" title=\"Право кнопки (задаёт модуль)\">${esc(cat.right || \"все\")}</span>\n      <input data-c=\"title\" placeholder=\"${esc(b.defTitle || cat.title || \"\")}\" value=\"${esc(custom)}\" title=\"Своё название (пусто = по умолчанию)\" oninput=\"tgMenuRename(this)\" onchange=\"tgMenuChanged(this)\">\n      <select data-c=\"width\" onchange=\"tgMenuChanged(this)\">${wOpt}</select>\n      <button class=\"btn-mini\" onclick=\"tgMenuMove(this,-1)\">▲</button><button class=\"btn-mini\" onclick=\"tgMenuMove(this,1)\">▼</button>\n      <button class=\"btn-mini\" onclick=\"tgMenuHide(this)\">скрыть</button>`;\n    el.appendChild(ctl);\n  }\n  return el;\n}\n\nfunction tgMenuRename(inp) {\n  const el = inp.closest(\".tg-btn\");\n  const label = el ? el.querySelector(\":scope > span\") : null;\n  if (label) label.textContent = (inp.value || \"\").trim() || inp.placeholder || \"\";\n}\n\nfunction tgPreviewState() {\n  const btns = [];\n  document.querySelectorAll(\"#tgmenu-preview .tg-btn\").forEach((el) => {\n    const b = tgById(el.dataset.id);\n    const sel = el.querySelector('[data-c=\"width\"]');\n    const inp = el.querySelector('[data-c=\"title\"]');\n    const custom = inp ? (inp.value || \"\").trim() : \"\";\n    btns.push({ id: el.dataset.id, width: sel ? sel.value : (b.width || \"auto\"),\n                hidden: false, title: custom || null });\n  });\n  document.querySelectorAll(\"#tgmenu-hidden .mon-hidden-row\").forEach((row) => {\n    row.dataset.width = row.dataset.width || \"auto\";\n    btns.push({ id: row.dataset.id, width: row.dataset.width,\n                hidden: true, title: row.dataset.title || null });\n  });\n  return btns;\n}\n\nasync function tgMenuSave() {\n  try {\n    const r = await api(\"/api/admin/tgmenu\", {\n      method: \"POST\",\n      body: JSON.stringify({ buttons: tgPreviewState() }),\n    });\n    if (!(r.ok && r.data && r.data.ok)) {\n      showToast(\"Ошибка: \" + ((r.data && r.data.error) || r.status));\n    }\n  } catch (e) {\n    showToast(\"Сбой: \" + e);\n  }\n}\n\nfunction toggleTgMenuEdit() {\n  tgMenuEditMode = !tgMenuEditMode;\n  const b = $(\"#tgmenu-edit-toggle\");\n  if (b) b.textContent = tgMenuEditMode ? \"Готово\" : \"Редактировать\";\n  renderTgMenuView();\n}\n\nfunction tgMenuChanged() { tgMenuSave().then(() => renderTgMenuView()); }\n\nfunction tgMenuMove(btn, dir) {\n  const el = btn.closest(\".tg-btn\");\n  if (!el) return;\n  // двигаем внутри ряда; через границу ряда — переносим целиком\n  const row = el.parentNode;\n  const sib = dir < 0 ? el.previousElementSibling : el.nextElementSibling;\n  if (sib) {\n    row.insertBefore(el, dir < 0 ? sib : sib.nextSibling);\n  } else {\n    const rows = [...row.parentNode.querySelectorAll(\".tg-row\")];\n    const i = rows.indexOf(row);\n    const other = rows[i + dir];\n    if (!other) return;\n    if (dir < 0) other.appendChild(el);\n    else other.insertBefore(el, other.firstChild);\n    if (!row.children.length) row.parentNode.removeChild(row);\n  }\n  tgMenuSave();\n}\n\nfunction tgMenuHide(btn) {\n  const el = btn.closest(\".tg-btn\");\n  if (!el) return;\n  const sel = el.querySelector('[data-c=\"width\"]');\n  const w = sel ? sel.value : ((tgById(el.dataset.id) || {}).width || \"auto\");\n  el.parentNode.removeChild(el);\n  document.querySelectorAll(\"#tgmenu-preview .tg-row\").forEach((r) => {\n    if (!r.children.length) r.parentNode.removeChild(r);\n  });\n  const strip = $(\"#tgmenu-hidden\");\n  const b = tgById(el.dataset.id);\n  if (strip) {\n    const s = document.createElement(\"div\");\n    s.className = \"mon-hidden-row\";\n    s.dataset.id = el.dataset.id;\n    s.dataset.width = w;\n    const ti = el.querySelector('[data-c=\"title\"]');\n    if (ti && (ti.value || \"\").trim()) s.dataset.title = ti.value.trim();\n    s.innerHTML = `<b></b> <span class=\"muted\"></span> <button class=\"btn-mini\">показать</button>`;\n    s.querySelector(\"b\").textContent = b.title || el.dataset.id;\n    s.querySelector(\"span\").textContent = el.dataset.id;\n    s.querySelector(\"button\").onclick = () => tgMenuShow(el.dataset.id);\n    strip.appendChild(s);\n    strip.classList.remove(\"hidden\");\n  }\n  tgMenuSave();\n}\n\nfunction tgMenuShow(id) {\n  const strip = $(\"#tgmenu-hidden\");\n  const row = strip ? strip.querySelector(`[data-id=\"${CSS.escape(id)}\"]`) : null;\n  if (row) row.parentNode.removeChild(row);\n  tgMenuSave().then(() => renderTgMenuView());\n}\n\n/* ---------------- система ---------------- */\n\nasync function loadSystem() {\n  const need = state.me && (state.me.is_admin || state.me.rights.manage_admins || state.me.rights.restart_bot);\n  if (!need) { showToast(\"Нет права manage_admins/restart_bot\"); showView(\"dashboard\"); return; }\n  const r = state.me.rights || {};\n  $(\"#sys-restart-bot\").style.display = r.restart_bot || r.all ? \"\" : \"none\";\n  $(\"#sys-restart-machine\").style.display = r.restart_machine || r.all ? \"\" : \"none\";\n  $(\"#sys-shutdown\").style.display = r.shutdown_host || r.all ? \"\" : \"none\";\n}\n\nasync function sysRestartBot() {\n  if (!confirm(\"Перезапустить бота?\")) return;\n  const btn = $(\"#sys-restart-bot\");\n  if (btn) btn.disabled = true;\n  const r = await api(\"/api/admin/system\", { method: \"POST\", body: JSON.stringify({ action: \"restart_bot\" }) });\n  if (btn) btn.disabled = false;\n  if (!r.ok) { showToast((r.data && r.data.error) || \"Ошибка\"); return; }\n  showToast(\"Бот перезапускается...\");\n}\n\nasync function sysRestartMachine() {\n  if (!confirm(\"Перезагрузить хост? Это прервёт все соединения.\")) return;\n  const btn = $(\"#sys-restart-machine\");\n  if (btn) btn.disabled = true;\n  const r = await api(\"/api/admin/system\", { method: \"POST\", body: JSON.stringify({ action: \"restart_machine\" }) });\n  if (btn) btn.disabled = false;\n  if (!r.ok) { showToast((r.data && r.data.error) || \"Ошибка\"); return; }\n  showToast(\"Хост перезагружается...\");\n}\n\nasync function sysShutdown() {\n  if (!confirm(\"Выключить хост? Это прервёт все соединения.\")) return;\n  const btn = $(\"#sys-shutdown\");\n  if (btn) btn.disabled = true;\n  const r = await api(\"/api/admin/system\", { method: \"POST\", body: JSON.stringify({ action: \"shutdown\" }) });\n  if (btn) btn.disabled = false;\n  if (!r.ok) { showToast((r.data && r.data.error) || \"Ошибка\"); return; }\n  showToast(\"Хост выключается...\");\n}\n\n$(\"#sys-restart-bot\")?.addEventListener(\"click\", sysRestartBot);\n$(\"#sys-restart-machine\")?.addEventListener(\"click\", sysRestartMachine);\n$(\"#sys-shutdown\")?.addEventListener(\"click\", sysShutdown);\n/* ---------------- скрипты (run_scripts) ---------------- */\n\nlet scrChosen = \"\";\nasync function loadScripts() {\n  if (!state.me || !(state.me.is_admin || state.me.rights.run_scripts)) { showToast(\"Нет права run_scripts\"); showView(\"dashboard\"); return; }\n  const r = await api(\"/api/admin/scripts\");\n  const tb = $(\"#scr-table tbody\");\n  if (r.status === 401 || r.status === 403) { tb.innerHTML = `<tr><td colspan=\"4\" class=\"muted\">Нет доступа</td></tr>`; return; }\n  if (!r.ok) { tb.innerHTML = `<tr><td colspan=\"4\" class=\"muted\">Ошибка: ${esc(r.data.error||\"\")}</td></tr>`; return; }\n  const items = r.data.scripts || [];\n  tb.innerHTML = items.length ? items.map((s)=> `\n    <tr>\n      <td class=\"mono\">${esc(s.name)}</td>\n      <td class=\"muted\">${s.size ? (s.size/1024).toFixed(1)+\" КБ\" : \"—\"}</td>\n      <td class=\"muted\">${esc(s.mtime||\"\")}</td>\n      <td class=\"actions\"><div style=\"display:flex;gap:4px;flex-wrap:wrap;align-items:center\">\n        <button class=\"btn-mini\" title=\"Выбрать для запуска\" onclick=\"chooseScript(${escJs(s.name)})\">Выбрать</button>\n        <button class=\"btn-mini\" title=\"Редактировать\" onclick=\"scrEdit(${escJs(s.name)})\">✏️</button>\n        ${s.name !== \"service_manager.sh\" ? `<button class=\"btn-mini danger\" title=\"Удалить скрипт\" onclick=\"scrDelete(${escJs(s.name)},this)\">🗑</button>` : \"\"}\n      </div></td>\n    </tr>`).join(\"\") : `<tr><td colspan=\"4\" class=\"muted\">скриптов нет</td></tr>`;\n  $(\"#scr-info\").textContent = `найдено: ${items.length}`;\n}\nfunction chooseScript(name) { scrChosen = name; $(\"#scr-chosen\").textContent = name; $(\"#scr-out\").textContent = \"\"; }\n/* Редактор скриптов */\nfunction scrEditHide() { $(\"#scr-edit-card\").classList.add(\"hidden\"); }\nfunction scrNew() {\n  $(\"#scr-edit-title\").textContent = \"Новый скрипт\";\n  const nm = $(\"#scr-name\");\n  nm.value = \"\";\n  nm.readOnly = false;\n  $(\"#scr-text\").value = \"#!/usr/bin/env bash\\nset -euo pipefail\\n\\n\";\n  $(\"#scr-edit-msg\").textContent = \"\";\n  $(\"#scr-edit-card\").classList.remove(\"hidden\");\n  $(\"#scr-edit-card\").scrollIntoView({ block: \"nearest\" });\n  nm.focus();\n}\nasync function scrEdit(name) {\n  $(\"#scr-edit-title\").textContent = \"Скрипт \" + name;\n  const nm = $(\"#scr-name\");\n  nm.value = name;\n  nm.readOnly = true;\n  $(\"#scr-edit-msg\").textContent = \"загрузка…\";\n  $(\"#scr-edit-card\").classList.remove(\"hidden\");\n  const r = await api(\"/api/admin/scripts/content?name=\" + encodeURIComponent(name));\n  if (!r.ok) { $(\"#scr-edit-msg\").textContent = \"Ошибка: \" + ((r.data && r.data.error) || \"\"); return; }\n  $(\"#scr-text\").value = r.data.content || \"\";\n  $(\"#scr-edit-msg\").textContent = \"\";\n  $(\"#scr-edit-card\").scrollIntoView({ block: \"nearest\" });\n}\nasync function scrSave(btn) {\n  const name = ($(\"#scr-name\").value || \"\").trim();\n  const content = $(\"#scr-text\").value || \"\";\n  const msg = $(\"#scr-edit-msg\");\n  if (!name) { msg.textContent = \"Укажите имя файла\"; return; }\n  if (btn) btn.disabled = true;\n  msg.textContent = \"сохранение…\";\n  const r = await api(\"/api/admin/scripts/save\", { method: \"POST\", body: JSON.stringify({ name, content }) });\n  if (btn) btn.disabled = false;\n  if (!r.ok) { msg.textContent = \"Ошибка: \" + ((r.data && r.data.error) || \"\"); return; }\n  msg.textContent = \"Сохранено: \" + name;\n  showToast(\"Скрипт сохранён (bash -n чист)\");\n  loadScripts();\n}\nasync function scrDelete(name, btn) {\n  if (!confirm(`Удалить скрипт ${name} ?`)) return;\n  if (btn) btn.disabled = true;\n  const r = await api(\"/api/admin/scripts/delete\", { method: \"POST\", body: JSON.stringify({ name }) });\n  if (btn) btn.disabled = false;\n  if (!r.ok) { showToast((r.data && r.data.error) || \"Ошибка\"); return; }\n  showToast(`Скрипт ${name} удалён`);\n  loadScripts();\n}\nasync function runScript() {\n  if (!scrChosen) { showToast(\"Выберите скрипт\"); return; }\n  const params = ($(\"#scr-params\").value||\"\").trim();\n  if (params && /[;&|`$()]/.test(params)) { showToast(\"Недопустимые символы в параметрах\"); return; }\n  if (params && !confirm(`Запустить ${scrChosen} ${params}?`)) return;\n  if (!params && !confirm(`Запустить ${scrChosen} ?`)) return;\n  $(\"#scr-out\").textContent = \"выполняется…\";\n  const r = await api(\"/api/admin/scripts\", { method:\"POST\", body: JSON.stringify({script: scrChosen, params}) });\n  $(\"#scr-out\").textContent = r.data.ok ? r.data.output : (\"⚠️ \"+(r.data.error||\"Ошибка\"));\n  showToast(r.data.ok ? \"Готово\" : (r.data.error||\"Ошибка\"));\n}\n$(\"#scr-table\")?.addEventListener(\"click\", (e)=> { if (e.target.tagName===\"BUTTON\") e.preventDefault(); });\n$(\"#scr-chosen\") && ($(\"#scr-chosen\").onclick = ()=> { if (scrChosen) chooseScript(scrChosen); });\n// связь кнопки выбора уже через inline onclick; добавим отдельную кнопку запуска через Enter в поле параметров\n$(\"#scr-params\")?.addEventListener(\"keydown\", (e)=> { if (e.key===\"Enter\") { e.preventDefault(); runScript(); } });\n// динамическая кнопка запуска — добавим после таблицы\nif (!$(\"#scr-run\")) {\n  const btn = document.createElement(\"button\");\n  btn.id = \"scr-run\"; btn.className = \"btn-mini\"; btn.textContent = \"Запустить выбранный\";\n  btn.addEventListener(\"click\", runScript);\n  const card = $(\"#view-scripts .card:last-child\");\n  if (card) card.querySelector(\"h3\").after(btn);\n}\n\nwindow.BotUI.registerService('view_user_messages', 'messages');\nwindow.BotUI.registerView('messages', loadMessages);\nwindow.BotUI.registerService('tgmenu_edit', 'tgmenu');\nwindow.BotUI.registerView('tgmenu', renderTgMenuView);\nwindow.BotUI.registerService('system', 'system');\nwindow.BotUI.registerView('system', loadSystem);\nwindow.BotUI.registerService('run_scripts', 'scripts');\nwindow.BotUI.registerView('scripts', loadScripts);\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* admin — модульные вьюхи (modules/admin/web/view.js). */</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* ---------------- сообщения пользователей (view_user_messages) -------- */</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> msgState</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { q: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, userId: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, limit: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">50</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, offset: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, total: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, users: [], sort: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, filterGroup: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> };</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">let</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgTimer </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> loadMessages</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">state.me </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(state.me.is_admin </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state.me.rights.view_user_messages)) {</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">    showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Нет права view_user_messages\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">    showView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"dashboard\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> params</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> new</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> URLSearchParams</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ limit: </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">String</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(msgState.limit), offset: </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">String</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(msgState.offset) });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (msgState.q) params.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"q\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, msgState.q);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (msgState.userId) params.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"user_id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, msgState.userId);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (msgState.sort) params.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"sort\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, msgState.sort);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (msgState.filterGroup) params.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">set</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"filter_group\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, msgState.filterGroup);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/messages?\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> params.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">toString</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">());</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> tbody</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msg-table tbody\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> info</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msg-info\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (r.status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 401</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 403</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    tbody.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;tr>&#x3C;td colspan=\"3\" class=\"muted\">Нет доступа (нужно view_user_messages)&#x3C;/td>&#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (info) info.textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    tbody.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;tr>&#x3C;td colspan=\"3\" class=\"muted\">Ошибка: ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">error</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td>&#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  msgState.total </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.total </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  msgState.users </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.users </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  renderMsgUsers</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> msgs</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.messages </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">msgs.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    tbody.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;tr>&#x3C;td colspan=\"3\" class=\"muted\">сообщений не найдено — поправите поиск/фильтр&#x3C;/td>&#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  } </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    tbody.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgs.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">m</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">      const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> chatBadge</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m.is_group </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;br>&#x3C;span class=\"pill ns-telegram\" style=\"font-size:10px\">👥 ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">chat_title</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"группа\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} &#x3C;span class=\"mono\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">chat_id</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>&#x3C;/span>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;br>&#x3C;span class=\"pill\" style=\"font-size:10px\">👤 личка&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">      return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;tr></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        &#x3C;td class=\"mono muted\" style=\"white-space:nowrap\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">time_short</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        &#x3C;td>&#x3C;span class=\"pill off mono\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user_id</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>&#x3C;br>&#x3C;span class=\"muted\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">chatBadge</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        &#x3C;td style=\"white-space:pre-wrap;word-break:break-word;max-width:520px\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">text</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> pages</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Math.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">max</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, Math.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">ceil</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(msgState.total </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">/</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgState.limit));</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> cur</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Math.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">floor</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(msgState.offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">/</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgState.limit) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msg-page\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">msgState</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">total</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} сообщ. · стр. ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">cur</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} / ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">pages</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msg-prev\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgState.offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msg-next\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgState.offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgState.limit </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">>=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgState.total;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (info) info.textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgState.q </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgState.userId </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `фильтр: ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">msgState</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">q</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"поиск «\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgState</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">q</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"» \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"}${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">msgState</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">userId</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"пользователь \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgState</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">userId</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"}`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> renderMsgUsers</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sel</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msg-user\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">sel) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> prev</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgState.userId;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  sel.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;option value=\"\">Все пользователи&#x3C;/option>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgState.users.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">u</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    `&#x3C;option value=\"${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">u</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user_id</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">u</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> u</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user_id</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} (${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">u</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">user_id</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}) · ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">u</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">count</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/option>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  sel.value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> prev;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (sel.value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!==</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> prev) msgState.userId </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msg-q\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)?.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">addEventListener</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"input\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, (</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">e</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  clearTimeout</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(msgTimer);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  msgTimer </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> setTimeout</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { msgState.q </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e.target.value.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">trim</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); msgState.offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadMessages</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); }, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">350</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">});</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msg-user\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)?.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">addEventListener</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"change\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, (</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">e</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { msgState.userId </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e.target.value; msgState.offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadMessages</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); });</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msg-limit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)?.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">addEventListener</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"change\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, (</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">e</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { msgState.limit </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> parseInt</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e.target.value,</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">10</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 50</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; msgState.offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadMessages</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); });</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msg-group-filter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)?.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">addEventListener</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"change\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, (</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">e</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { msgState.filterGroup </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e.target.value; msgState.offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadMessages</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); });</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msg-sort\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)?.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">addEventListener</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"change\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, (</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">e</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { msgState.sort </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e.target.value; msgState.offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadMessages</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); });</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msg-refresh\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)?.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">addEventListener</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"click\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, () </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> loadMessages</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">());</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msg-prev\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)?.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">addEventListener</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"click\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, () </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (msgState.offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { msgState.offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Math.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">max</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, msgState.offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgState.limit); </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadMessages</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); } });</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#msg-next\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)?.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">addEventListener</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"click\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, () </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (msgState.offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgState.limit </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgState.total) { msgState.offset </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msgState.limit; </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadMessages</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); } });</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">let</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tgMenuEditMode </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">let</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tgMenuCatalog </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> TG_WIDTHS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"auto\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"2/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"3/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1/3\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"2/3\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"full\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">];</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> TG_SLOTS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0.25</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"2/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0.5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"3/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0.75</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1/3\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> /</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"2/3\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> /</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tgWidthLabel</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">w</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { auto: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"авто\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"2/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"2/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"3/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"3/4\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">           \"1/3\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1/3\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"2/3\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"2/3\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, full: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"вся строка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> }[w] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> w;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> renderTgMenuView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> prev</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#tgmenu-preview\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">prev) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  let</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/tgmenu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (r.status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 403</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Нет права menu_edit\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"dashboard\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (r.ok) d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  } </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">catch</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d.catalog) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  tgMenuCatalog </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.catalog;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> cfg</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> e</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> of</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (d.layout </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [])) cfg[e.id] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> btns</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.catalog.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">b</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> e</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cfg[b.id] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">...</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">b, width: e.width </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"auto\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, hidden: </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e.hidden,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">             title: e.title </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b.title, defTitle: b.title };</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> tgl</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#tgmenu-edit-toggle\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (tgl) tgl.classList.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">remove</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  renderTgPreview</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(btns);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tgById</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (tgMenuCatalog </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">find</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">b</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b.id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> id) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> renderTgPreview</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> prev</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#tgmenu-preview\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> strip</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#tgmenu-hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  prev.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (strip) strip.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">  // жадная упаковка как в движке: full — своя строка, остальные добирают ряд.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  let</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> row </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, fill </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> flush</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> () </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { row </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; fill </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; };</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> newRow</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> () </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    row </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">createElement</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"div\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    row.className </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"tg-row\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    prev.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">appendChild</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(row);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    fill </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  };</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> b</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> of</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> btns) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (b.hidden) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">      if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (strip </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tgMenuEditMode) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> s</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">createElement</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"div\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        s.className </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"mon-hidden-row\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        s.dataset.id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b.id;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        s.dataset.width </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b.width </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"auto\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (b.title </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b.title </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!==</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">tgById</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(b.id).title </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) s.dataset.title </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b.title;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        s.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;b>&#x3C;/b> &#x3C;span class=\"muted\">&#x3C;/span> &#x3C;button class=\"btn-mini\">показать&#x3C;/button>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        s.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"b\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b.title;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        s.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"span\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b.id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">tgById</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(b.id).module </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" · \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tgById</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(b.id).module </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        s.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"button\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">onclick</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> () </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tgMenuShow</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(b.id);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        strip.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">appendChild</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(s);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">      continue</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> w</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b.width </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"auto\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> need</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> w </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"full\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TG_SLOTS</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">[w] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0.5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (w </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"full\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">row </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fill </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> need </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1e-9</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">      if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (row </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> row.children.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ===</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) prev.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">removeChild</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(row);</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">      newRow</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">      if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (w </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"full\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        row.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">appendChild</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">tgButtonEl</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(b, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">));</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">        flush</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        continue</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    row.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">appendChild</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">tgButtonEl</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(b, need));</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    fill </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> need;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (row </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> row.children.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ===</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) prev.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">removeChild</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(row);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (strip) strip.classList.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">toggle</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">tgMenuEditMode);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tgButtonEl</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">b</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">need</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> el</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">createElement</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"div\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  el.className </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"tg-btn\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  el.dataset.id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b.id;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  el.style.flex </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> need </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">>=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"1 1 100%\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `1 1 ${</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">need</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> *</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 100</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">toFixed</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}%`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  el.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;span>&#x3C;/span>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  el.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"span\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b.title;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (tgMenuEditMode) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> cat</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tgById</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(b.id);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> custom</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (b.title </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!==</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (b.defTitle </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cat.title)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b.title </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ctl</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">createElement</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"div\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    ctl.className </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"mon-ctl\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> wOpt</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> TG_WIDTHS</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">v</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      `&#x3C;option value=\"${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">v</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}\" ${</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">b</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">width</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"auto\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> v</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"selected\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"}>${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">tgWidthLabel</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">v</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/option>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    ctl.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;span class=\"mon-mod\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">cat</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">module</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;span class=\"mon-mod\" title=\"Право кнопки (задаёт модуль)\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">cat</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">right</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"все\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;input data-c=\"title\" placeholder=\"${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">b</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">defTitle</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cat</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">title</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}\" value=\"${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">custom</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}\" title=\"Своё название (пусто = по умолчанию)\" oninput=\"tgMenuRename(this)\" onchange=\"tgMenuChanged(this)\"></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;select data-c=\"width\" onchange=\"tgMenuChanged(this)\">${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">wOpt</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/select></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;button class=\"btn-mini\" onclick=\"tgMenuMove(this,-1)\">▲&#x3C;/button>&#x3C;button class=\"btn-mini\" onclick=\"tgMenuMove(this,1)\">▼&#x3C;/button></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;button class=\"btn-mini\" onclick=\"tgMenuHide(this)\">скрыть&#x3C;/button>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    el.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">appendChild</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctl);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> el;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tgMenuRename</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">inp</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> el</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> inp.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">closest</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".tg-btn\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> label</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> el </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> el.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\":scope > span\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (label) label.textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (inp.value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">trim</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> inp.placeholder </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tgPreviewState</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> btns</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelectorAll</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#tgmenu-preview .tg-btn\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">forEach</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">el</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> b</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tgById</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(el.dataset.id);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sel</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> el.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'[data-c=\"width\"]'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> inp</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> el.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'[data-c=\"title\"]'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> custom</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> inp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (inp.value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">trim</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    btns.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">push</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ id: el.dataset.id, width: sel </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sel.value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (b.width </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"auto\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                hidden: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, title: custom </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> });</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  });</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelectorAll</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#tgmenu-hidden .mon-hidden-row\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">forEach</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">row</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    row.dataset.width </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> row.dataset.width </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"auto\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    btns.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">push</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ id: row.dataset.id, width: row.dataset.width,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                hidden: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, title: row.dataset.title </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> });</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> btns;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tgMenuSave</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/tgmenu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      method: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">      body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ buttons: </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">tgPreviewState</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() }),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(r.ok </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.ok)) {</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">      showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка: \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.status));</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  } </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">catch</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (e) {</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">    showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Сбой: \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> toggleTgMenuEdit</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  tgMenuEditMode </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">tgMenuEditMode;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> b</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#tgmenu-edit-toggle\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (b) b.textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> tgMenuEditMode </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Готово\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Редактировать\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  renderTgMenuView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tgMenuChanged</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">tgMenuSave</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">().</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">then</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> renderTgMenuView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">()); }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tgMenuMove</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">dir</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> el</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> btn.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">closest</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".tg-btn\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">el) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">  // двигаем внутри ряда; через границу ряда — переносим целиком</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> row</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> el.parentNode;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sib</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dir </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> el.previousElementSibling </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> el.nextElementSibling;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (sib) {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    row.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">insertBefore</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(el, dir </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sib </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sib.nextSibling);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  } </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> rows</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">...</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">row.parentNode.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelectorAll</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".tg-row\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)];</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> i</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rows.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">indexOf</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(row);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> other</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rows[i </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dir];</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">other) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (dir </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) other.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">appendChild</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(el);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> other.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">insertBefore</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(el, other.firstChild);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">row.children.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) row.parentNode.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">removeChild</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(row);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  tgMenuSave</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tgMenuHide</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> el</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> btn.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">closest</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\".tg-btn\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">el) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sel</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> el.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'[data-c=\"width\"]'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> w</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sel </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sel.value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">tgById</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(el.dataset.id) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}).width </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"auto\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  el.parentNode.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">removeChild</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(el);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelectorAll</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#tgmenu-preview .tg-row\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">forEach</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">r</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.children.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) r.parentNode.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">removeChild</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(r);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> strip</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#tgmenu-hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> b</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tgById</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(el.dataset.id);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (strip) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> s</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">createElement</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"div\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    s.className </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"mon-hidden-row\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    s.dataset.id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> el.dataset.id;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    s.dataset.width </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> w;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ti</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> el.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'[data-c=\"title\"]'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (ti </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (ti.value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">trim</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">()) s.dataset.title </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ti.value.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">trim</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    s.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;b>&#x3C;/b> &#x3C;span class=\"muted\">&#x3C;/span> &#x3C;button class=\"btn-mini\">показать&#x3C;/button>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    s.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"b\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b.title </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> el.dataset.id;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    s.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"span\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> el.dataset.id;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    s.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"button\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">onclick</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> () </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tgMenuShow</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(el.dataset.id);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    strip.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">appendChild</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(s);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    strip.classList.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">remove</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  tgMenuSave</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> tgMenuShow</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> strip</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#tgmenu-hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> row</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> strip </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> strip.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">`[data-id=\"${</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">CSS</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escape</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">id</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}\"]`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (row) row.parentNode.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">removeChild</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(row);</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  tgMenuSave</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">().</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">then</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> renderTgMenuView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">());</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* ---------------- система ---------------- */</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> loadSystem</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> need</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state.me </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (state.me.is_admin </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state.me.rights.manage_admins </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state.me.rights.restart_bot);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">need) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Нет права manage_admins/restart_bot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"dashboard\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state.me.rights </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#sys-restart-bot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).style.display </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.restart_bot </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.all </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"none\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#sys-restart-machine\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).style.display </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.restart_machine </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.all </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"none\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#sys-shutdown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).style.display </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.shutdown_host </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.all </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"none\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> sysRestartBot</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">confirm</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Перезапустить бота?\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> btn</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#sys-restart-bot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/system\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ action: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart_bot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> }) });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Бот перезапускается...\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> sysRestartMachine</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">confirm</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Перезагрузить хост? Это прервёт все соединения.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> btn</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#sys-restart-machine\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/system\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ action: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"restart_machine\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> }) });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Хост перезагружается...\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> sysShutdown</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">confirm</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Выключить хост? Это прервёт все соединения.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> btn</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#sys-shutdown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/system\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ action: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"shutdown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> }) });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Хост выключается...\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#sys-restart-bot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)?.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">addEventListener</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"click\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, sysRestartBot);</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#sys-restart-machine\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)?.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">addEventListener</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"click\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, sysRestartMachine);</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#sys-shutdown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)?.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">addEventListener</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"click\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, sysShutdown);</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* ---------------- скрипты (run_scripts) ---------------- */</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">let</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> scrChosen </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> loadScripts</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">state.me </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(state.me.is_admin </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state.me.rights.run_scripts)) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Нет права run_scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"dashboard\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> tb</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-table tbody\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (r.status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 401</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 403</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { tb.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;tr>&#x3C;td colspan=\"4\" class=\"muted\">Нет доступа&#x3C;/td>&#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { tb.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;tr>&#x3C;td colspan=\"4\" class=\"muted\">Ошибка: ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">error</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td>&#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> items</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.scripts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  tb.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> items.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> items.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">s</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    &#x3C;tr></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td class=\"mono\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td class=\"muted\">${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">size</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">size</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">/</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1024</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">toFixed</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\" КБ\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"—\"}&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td class=\"muted\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mtime</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td class=\"actions\">&#x3C;div style=\"display:flex;gap:4px;flex-wrap:wrap;align-items:center\"></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        &#x3C;button class=\"btn-mini\" title=\"Выбрать для запуска\" onclick=\"chooseScript(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})\">Выбрать&#x3C;/button></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        &#x3C;button class=\"btn-mini\" title=\"Редактировать\" onclick=\"scrEdit(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})\">✏️&#x3C;/button></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"service_manager.sh\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;button class=\"btn-mini danger\" title=\"Удалить скрипт\" onclick=\"scrDelete(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">},this)\">🗑&#x3C;/button>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"}</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;/div>&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    &#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;tr>&#x3C;td colspan=\"4\" class=\"muted\">скриптов нет&#x3C;/td>&#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-info\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `найдено: ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">items</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> chooseScript</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { scrChosen </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name; </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-chosen\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name; </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-out\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* Редактор скриптов */</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> scrEditHide</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-edit-card\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).classList.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">add</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> scrNew</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-edit-title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Новый скрипт\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> nm</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  nm.value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  nm.readOnly </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-text\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"#!/usr/bin/env bash</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">set -euo pipefail</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-edit-msg\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-edit-card\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).classList.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">remove</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-edit-card\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">scrollIntoView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ block: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nearest\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> });</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  nm.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">focus</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> scrEdit</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-edit-title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Скрипт \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> nm</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  nm.value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  nm.readOnly </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-edit-msg\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"загрузка…\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-edit-card\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).classList.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">remove</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/scripts/content?name=\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> encodeURIComponent</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name));</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-edit-msg\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Ошибка: \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-text\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.content </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-edit-msg\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-edit-card\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">scrollIntoView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ block: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nearest\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> });</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> scrSave</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> name</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">trim</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> content</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-text\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).value </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> msg</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-edit-msg\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name) { msg.textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Укажите имя файла\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  msg.textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"сохранение…\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/scripts/save\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ name, content }) });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { msg.textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Ошибка: \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  msg.textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Сохранено: \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Скрипт сохранён (bash -n чист)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  loadScripts</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> scrDelete</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">btn</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">confirm</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">`Удалить скрипт ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} ?`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/scripts/delete\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ name }) });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (btn) btn.disabled </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">`Скрипт ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} удалён`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  loadScripts</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> runScript</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">scrChosen) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Выберите скрипт\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> params</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-params\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).value</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">trim</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (params </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> /</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">[;&#x26;|`$()]</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">test</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(params)) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Недопустимые символы в параметрах\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (params </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">confirm</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">`Запустить ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">scrChosen</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">params</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}?`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">params </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">confirm</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">`Запустить ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">scrChosen</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} ?`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-out\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"выполняется…\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({script: scrChosen, params}) });</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-out\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.ok </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.output </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"⚠️ \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(r.data.error</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">));</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(r.data.ok </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Готово\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (r.data.error</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">));</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-table\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)?.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">addEventListener</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"click\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, (</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">e</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (e.target.tagName</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"BUTTON\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) e.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">preventDefault</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); });</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-chosen\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-chosen\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">onclick</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ()</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (scrChosen) </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">chooseScript</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(scrChosen); });</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">// связь кнопки выбора уже через inline onclick; добавим отдельную кнопку запуска через Enter в поле параметров</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-params\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)?.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">addEventListener</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"keydown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, (</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">e</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (e.key</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Enter\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { e.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">preventDefault</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">runScript</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); } });</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">// динамическая кнопка запуска — добавим после таблицы</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#scr-run\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> btn</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> document.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">createElement</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"button\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  btn.id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"scr-run\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; btn.className </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"btn-mini\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; btn.textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Запустить выбранный\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  btn.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">addEventListener</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"click\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, runScript);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> card</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#view-scripts .card:last-child\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (card) card.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">querySelector</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"h3\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">after</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(btn);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">window.BotUI.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">registerService</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'view_user_messages'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'messages'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">window.BotUI.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">registerView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'messages'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, loadMessages);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">window.BotUI.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">registerService</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'tgmenu_edit'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'tgmenu'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">window.BotUI.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">registerView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'tgmenu'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, renderTgMenuView);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">window.BotUI.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">registerService</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'system'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'system'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">window.BotUI.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">registerView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'system'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, loadSystem);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">window.BotUI.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">registerService</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'run_scripts'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'scripts'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">window.BotUI.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">registerView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'scripts'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, loadScripts);</span></span>\n<span class=\"line\"></span></code></pre>"
        }
      ],
      "note": "Управление ботом",
      "_extra": {
        "api": "",
        "events_emitted": "[]",
        "events_subscribed": "[]",
        "audit_allow": "",
        "uses_rights": "",
        "tg_commands": "",
        "shared_routes": "",
        "config_schema": "config.schema.json",
        "env_example": ".env.example"
      }
    },
    {
      "name": "network",
      "version": "0.3.8",
      "category": "extra",
      "description": "Сеть: сканер устройств, WireGuard/Guacamole/fail2ban-мониторы, MAC-блокировки, сниффер.",
      "requires": [],
      "rights": [],
      "api_methods": [],
      "web_routes": [],
      "commit": "2d7d04615b31be9f5bbd91d5d61671182164bf39",
      "updated": "2026-09-16 16:35:50 +0300",
      "integrity": "sha256:d3b1ccc1fee0850c875620129b6dde2063fa3aa066fbc98ff4d793ce3938a6ef",
      "tree_url": "http://10.10.10.30:3000/euk0r/bot_modular/src/branch/main/modules/network",
      "raw_url": "http://10.10.10.30:3000/euk0r/bot_modular/raw/branch/main/modules/network/module.py",
      "files": [
        {
          "path": "README.md",
          "size": 3851,
          "content": "# network `0.3.8`\n\nКарта домашней сети: кто онлайн, кто новый, кто пропал.\n\nТип: Модуль. Категория: `extra`. Зависимостей нет.\n## Описание\nГлаза бота в локалке `10.10.10.0/24`. По кругу ходит nmap, сверяется с кешем и докладывает: новое устройство, переподключение, отключение (только после нескольких пропусков подряд — спасает дебаунс), смена IP. Мелкие повторы склеиваются агрегацией. Каждому устройству копятся статистика и открытые порты, имена подписываются вручную (`nmap_setname` принимается и из TG, и из веба), знакомые MAC — в игнор-лист.\n\nДоп. источники (fail2ban, WireGuard, Guacamole — код в своих модулях) подключаются через `register_monitor`. Длинные списки в TG режутся на чанки по 3000 символов.\n## Возможности\n- `scanner` `{}` → движок сканирования (кеш устройств)\n- `cache_file` `{}` → путь кеша устройств\n- `ignored_file` `{}` → путь игнор-листа\n- `devices_snapshot` `{}` → `{total, online, devices[]}` — обогащённый снимок для мониторинга\n- `register_monitor` `{name, factory, interval_s, type, silent}` → плагин доп. источника\n## Права\n- `nmap_manage` — Сканер сети (GET/POST /api/admin/nmap*)\n- `receive_notifications_nmap` — Уведомления сканера сети (новые/потерянные устройства)\n- `receive_notifications_nmap_edit` — Право менять себе уведомления сканера сети\n## Telegram\n- кнопка «📡 Сканирование» → меню, постраничные списки устройств и статистики (просмотр — право `view_net_status`, изменения — `nmap_manage`)\n- переименование устройств диалогом (`net_setname` / `nmap_setname`)\n## Web\n- `GET /api/network/devices` — устройства для мониторинга\n- `GET /api/admin/nmap` — устройства + игнор-лист\n- `POST /api/admin/nmap/name` — подписать имя `{mac, name}`\n- `POST /api/admin/nmap/ignore` — игнор `{action: add|remove, mac}`\n- `POST /api/network/nmap/delete` — удалить устройство (только веб, в TG удаления нет)\n- карточка «Сканер сети»\n## События шины\n- излучает `network.device_new`, `network.device_reconnected`, `network.device_disconnected`, `network.device_ip_changed` (по одному на устройство)\n## Конфигурация (`config.schema.json`)\n- обязательных нет; опциональные (дефолты):\n  - `SCAN_NETWORK_RANGE` = `10.10.10.0/24` — диапазон сканирования\n  - `CHECK_DURATION_NSCAN_LONG` = `30` — длинный цикл, сек\n  - `CHECK_DURATION_NSCAN_FAST` = `5` — быстрый цикл, сек\n  - `DEBOUNCE_COUNT` = `2` — пропусков до «пропал»\n  - `AGGREGATION_INTERVAL` = `300` — окно склейки уведомлений, сек\n  - `AGGREGATION_MAX_CHARS` = `3500` — лимит склеенного текста\n  - `NOTIF_MAX_RETRIES` = `3` — ретраи уведомлений\n  - `NOTIF_RETRY_BACKOFF` = `1.5` — backoff ретраев\n  - `F2B_ENABLED` = `1` — штатный монитор fail2ban вкл\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\"># network </span><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">`0.3.8`</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Карта домашней сети: кто онлайн, кто новый, кто пропал.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Тип: Модуль. Категория: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`extra`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">. Зависимостей нет.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Описание</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Глаза бота в локалке </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`10.10.10.0/24`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">. По кругу ходит nmap, сверяется с кешем и докладывает: новое устройство, переподключение, отключение (только после нескольких пропусков подряд — спасает дебаунс), смена IP. Мелкие повторы склеиваются агрегацией. Каждому устройству копятся статистика и открытые порты, имена подписываются вручную (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`nmap_setname`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> принимается и из TG, и из веба), знакомые MAC — в игнор-лист.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">Доп. источники (fail2ban, WireGuard, Guacamole — код в своих модулях) подключаются через </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`register_monitor`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">. Длинные списки в TG режутся на чанки по 3000 символов.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Возможности</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `scanner`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → движок сканирования (кеш устройств)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `cache_file`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → путь кеша устройств</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `ignored_file`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → путь игнор-листа</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `devices_snapshot`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`{total, online, devices[]}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — обогащённый снимок для мониторинга</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `register_monitor`</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `{name, factory, interval_s, type, silent}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> → плагин доп. источника</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Права</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `nmap_manage`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — Сканер сети (GET/POST /api/admin/nmap*)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `receive_notifications_nmap`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — Уведомления сканера сети (новые/потерянные устройства)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `receive_notifications_nmap_edit`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — Право менять себе уведомления сканера сети</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Telegram</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> кнопка «📡 Сканирование» → меню, постраничные списки устройств и статистики (просмотр — право </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`view_net_status`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, изменения — </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`nmap_manage`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> переименование устройств диалогом (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`net_setname`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> / </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`nmap_setname`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Web</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `GET /api/network/devices`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — устройства для мониторинга</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `GET /api/admin/nmap`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — устройства + игнор-лист</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `POST /api/admin/nmap/name`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — подписать имя </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`{mac, name}`</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `POST /api/admin/nmap/ignore`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — игнор </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`{action: add|remove, mac}`</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `POST /api/network/nmap/delete`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — удалить устройство (только веб, в TG удаления нет)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> карточка «Сканер сети»</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## События шины</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> излучает </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`network.device_new`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`network.device_reconnected`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`network.device_disconnected`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`network.device_ip_changed`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (по одному на устройство)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">## Конфигурация (</span><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">`config.schema.json`</span><span style=\"color:#005CC5;--shiki-light-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold\">)</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> обязательных нет; опциональные (дефолты):</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">  -</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `SCAN_NETWORK_RANGE`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> = </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`10.10.10.0/24`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — диапазон сканирования</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">  -</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `CHECK_DURATION_NSCAN_LONG`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> = </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`30`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — длинный цикл, сек</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">  -</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `CHECK_DURATION_NSCAN_FAST`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> = </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`5`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — быстрый цикл, сек</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">  -</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `DEBOUNCE_COUNT`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> = </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`2`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — пропусков до «пропал»</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">  -</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `AGGREGATION_INTERVAL`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> = </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`300`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — окно склейки уведомлений, сек</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">  -</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `AGGREGATION_MAX_CHARS`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> = </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`3500`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — лимит склеенного текста</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">  -</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `NOTIF_MAX_RETRIES`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> = </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`3`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — ретраи уведомлений</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">  -</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `NOTIF_RETRY_BACKOFF`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> = </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`1.5`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — backoff ретраев</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">  -</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> `F2B_ENABLED`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> = </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">`1`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> — штатный монитор fail2ban вкл</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "config.py",
          "size": 2842,
          "content": "\"\"\"Пакетный config-шим для network (scan пишет config.X).\n\nВНИМАНИЕ: macblock импортирует значения (from .config import Y) — поэтому\ninstall() обязан вызываться ДО импорта движков (порядок в module.py).\nПути кешей — data/cache/network (свежие, пишет сам сканер); зеркало\nпрод-кеша для чтения — через MONITOR_CACHE_DIR транспорта (только чтение).\nЦели уведомлений сканера — единый ADMIN_IDS модуля access (через ctx.api).\n\"\"\"\nimport os\n\n_vals: dict = {}\n\n\ndef _int(values: dict, key: str, default: int) -> int:\n    try:\n        return int(str(values.get(key, default) or default))\n    except (ValueError, TypeError):\n        return default\n\n\ndef install(values: dict, files: dict) -> None:\n    _vals.clear()\n    _vals.update(values)\n    _vals.update(files)\n    for k, d in ((\"CHECK_DURATION_NSCAN_LONG\", 30), (\"CHECK_DURATION_NSCAN_FAST\", 5),\n                 (\"DEBOUNCE_COUNT\", 2), (\"AGGREGATION_INTERVAL\", 300),\n                 (\"AGGREGATION_MAX_CHARS\", 3500), (\"NOTIF_MAX_RETRIES\", 3),\n                 (\"ROUTER_PORT\", 22), (\"TRAFFIC_MAX_SCANS\", 50),\n                 (\"TRAFFIC_MAX_PCAP_MB\", 20), (\"TRAFFIC_DEFAULT_DURATION\", 30),\n                 (\"TRAFFIC_MAX_DURATION\", 120), (\"TRAFFIC_DEFAULT_PACKETS\", 2000),\n                 (\"TRAFFIC_MAX_PACKETS\", 10000)):\n        _vals[k] = _int(values, k, d)\n    try:\n        _vals[\"NOTIF_RETRY_BACKOFF\"] = float(\n            str(values.get(\"NOTIF_RETRY_BACKOFF\", \"1.5\") or \"1.5\"))\n    except (ValueError, TypeError):\n        _vals[\"NOTIF_RETRY_BACKOFF\"] = 1.5\n    for k in (\"ROUTER_HOST\", \"ROUTER_USER\", \"ROUTER_PASSWORD\",\n              \"SCAN_NETWORK_RANGE\", \"F2B_ENABLED\"):\n        _vals.setdefault(k, (values.get(k) or \"\").strip())\n    if \"ROUTER_HOST\" not in _vals or not _vals[\"ROUTER_HOST\"]:\n        _vals[\"ROUTER_HOST\"] = (values.get(\"ROUTER_HOST\") or \"10.10.10.1\").strip()\n    if not _vals.get(\"ROUTER_USER\"):\n        _vals[\"ROUTER_USER\"] = (values.get(\"ROUTER_USER\") or \"root\").strip()\n    if not _vals.get(\"SCAN_NETWORK_RANGE\"):\n        _vals[\"SCAN_NETWORK_RANGE\"] = (\n            values.get(\"SCAN_NETWORK_RANGE\") or \"10.10.10.0/24\").strip()\n    prot = (values.get(\"MACBLOCK_PROTECTED\") or \"bc:24:11:39:ea:6c\").strip()\n    _vals[\"MACBLOCK_PROTECTED\"] = [m.strip().upper() for m in prot.split(\",\") if m.strip()]\n    _vals[\"ADMIN_PASS\"] = values.get(\"ADMIN_PASS\") or \"\"\n    for k, d in ((\"TRAFFIC_DEFAULT_DURATION\", 30), (\"TRAFFIC_DEFAULT_PACKETS\", 2000)):\n        _vals[k] = _int(values, k, d)\n\n\ndef __getattr__(name: str):\n    try:\n        return _vals[name]\n    except KeyError:\n        raise AttributeError(\"network.config: нет ключа %s\" % name) from None\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"Пакетный config-шим для network (scan пишет config.X).</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">ВНИМАНИЕ: macblock импортирует значения (from .config import Y) — поэтому</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">install() обязан вызываться ДО импорта движков (порядок в module.py).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Пути кешей — data/cache/network (свежие, пишет сам сканер); зеркало</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">прод-кеша для чтения — через MONITOR_CACHE_DIR транспорта (только чтение).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Цели уведомлений сканера — единый ADMIN_IDS модуля access (через ctx.api).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">_vals: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(values: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, key: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, default: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(values.get(key, default) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> default))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TypeError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> default</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> install</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(values: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, files: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _vals.clear()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _vals.update(values)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _vals.update(files)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k, d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"CHECK_DURATION_NSCAN_LONG\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">30</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"CHECK_DURATION_NSCAN_FAST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                 (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"DEBOUNCE_COUNT\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"AGGREGATION_INTERVAL\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">300</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                 (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"AGGREGATION_MAX_CHARS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">3500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"NOTIF_MAX_RETRIES\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                 (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ROUTER_PORT\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">22</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"TRAFFIC_MAX_SCANS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">50</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                 (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"TRAFFIC_MAX_PCAP_MB\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">20</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"TRAFFIC_DEFAULT_DURATION\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">30</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                 (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"TRAFFIC_MAX_DURATION\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">120</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"TRAFFIC_DEFAULT_PACKETS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2000</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                 (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"TRAFFIC_MAX_PACKETS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">10000</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        _vals[k] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _int(values, k, d)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        _vals[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"NOTIF_RETRY_BACKOFF\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> float</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(values.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"NOTIF_RETRY_BACKOFF\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1.5\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"1.5\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TypeError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        _vals[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"NOTIF_RETRY_BACKOFF\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1.5</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ROUTER_HOST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ROUTER_USER\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ROUTER_PASSWORD\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">              \"SCAN_NETWORK_RANGE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"F2B_ENABLED\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        _vals.setdefault(k, (values.get(k) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip())</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"ROUTER_HOST\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _vals </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _vals[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ROUTER_HOST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        _vals[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ROUTER_HOST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (values.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ROUTER_HOST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"10.10.10.1\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _vals.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ROUTER_USER\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        _vals[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ROUTER_USER\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (values.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ROUTER_USER\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"root\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _vals.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SCAN_NETWORK_RANGE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        _vals[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SCAN_NETWORK_RANGE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            values.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SCAN_NETWORK_RANGE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"10.10.10.0/24\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    prot </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (values.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MACBLOCK_PROTECTED\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"bc:24:11:39:ea:6c\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _vals[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MACBLOCK_PROTECTED\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [m.strip().upper() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> prot.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\",\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m.strip()]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _vals[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ADMIN_PASS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> values.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ADMIN_PASS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> k, d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"TRAFFIC_DEFAULT_DURATION\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">30</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"TRAFFIC_DEFAULT_PACKETS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2000</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        _vals[k] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _int(values, k, d)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> __getattr__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _vals[name]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> KeyError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> AttributeError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network.config: нет ключа </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "config.schema.json",
          "size": 347,
          "content": "{\n  \"required\": [],\n  \"optional\": {\n    \"SCAN_NETWORK_RANGE\": \"10.10.10.0/24\",\n    \"CHECK_DURATION_NSCAN_LONG\": \"30\",\n    \"CHECK_DURATION_NSCAN_FAST\": \"5\",\n    \"DEBOUNCE_COUNT\": \"2\",\n    \"AGGREGATION_INTERVAL\": \"300\",\n    \"AGGREGATION_MAX_CHARS\": \"3500\",\n    \"NOTIF_MAX_RETRIES\": \"3\",\n    \"NOTIF_RETRY_BACKOFF\": \"1.5\",\n    \"F2B_ENABLED\": \"1\"\n  }\n}",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">{</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"required\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: [],</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">  \"optional\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"SCAN_NETWORK_RANGE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"10.10.10.0/24\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"CHECK_DURATION_NSCAN_LONG\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"30\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"CHECK_DURATION_NSCAN_FAST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"5\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"DEBOUNCE_COUNT\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"2\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"AGGREGATION_INTERVAL\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"300\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"AGGREGATION_MAX_CHARS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"3500\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"NOTIF_MAX_RETRIES\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"3\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"NOTIF_RETRY_BACKOFF\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1.5\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">    \"F2B_ENABLED\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span></code></pre>"
        },
        {
          "path": "fail2ban_mon.py",
          "size": 6751,
          "content": "import subprocess\nimport json\nimport os\nfrom datetime import datetime\nimport logging\n\nlogger = logging.getLogger(__name__)\n\ndef parse_event_day(event_time):\n    \"\"\"\n    Извлекает дату из event_time.\n    Ожидаемый формат: \"YYYY-MM-DD HH:MM:SS\", возвращает \"YYYY-MM-DD\".\n    \"\"\"\n    return event_time.split()[0]\n\nclass Fail2BanMonitor:\n    def __init__(self, cache_file, monitoring_script):\n        \"\"\"\n        :param cache_file: Путь к файлу кеша (например, \"fail2ban_cache.json\").\n        :param monitoring_script: Путь к shell‑скрипту (например, \"./fail2ban_parse.sh\").\n               Скрипт должен выводить валидный JSON‑массив объектов с ключами:\n                  - \"service\" (например, \"guacamole\")\n                  - \"time\"   (формат \"YYYY-MM-DD HH:MM:SS\")\n                  - \"ip\"\n                  - \"action\" (в данном случае всегда \"Ban\")\n        \"\"\"\n        self.cache_file = cache_file\n        self.monitoring_script = monitoring_script\n\n    def load_cache(self):\n        \"\"\"\n        Загружает кеш из файла.\n        Структура кеша: {\n              \"ip1\": [event1, event2, ...],\n              \"ip2\": [...],\n              ...\n        }\n        При загрузке остаются только события текущего дня.\n        \"\"\"\n        if not os.path.exists(self.cache_file):\n            return {}\n        try:\n            with open(self.cache_file, \"r\") as f:\n                cache = json.load(f)\n            current_day = datetime.now().strftime(\"%Y-%m-%d\")\n            new_cache = {}\n            for ip, events in cache.items():\n                filtered = [event for event in events if event.get(\"time\") and parse_event_day(event[\"time\"]) == current_day]\n                if filtered:\n                    new_cache[ip] = filtered\n            return new_cache\n        except Exception as e:\n            logger.error(f\"Ошибка при загрузке кеша: {e}\")\n            return {}\n\n    def save_cache(self, cache):\n        \"\"\"Сохраняет кеш в файл.\"\"\"\n        try:\n            with open(self.cache_file, \"w\") as f:\n                json.dump(cache, f, indent=4)\n        except Exception as e:\n            logger.error(f\"Ошибка при сохранении кеша: {e}\")\n\n    def process_script_output(self):\n        \"\"\"\n        Выполняет shell‑скрипт для парсинга логов Fail2Ban.\n        Ожидается, что скрипт выводит валидный JSON‑массив объектов.\n        \"\"\"\n        try:\n            result = subprocess.run(\n                [self.monitoring_script],\n                capture_output=True,\n                text=True,\n                check=True\n            )\n            output = result.stdout.strip()\n            data = json.loads(output)\n            return data\n        except json.JSONDecodeError as e:\n            logger.error(f\"Скрипт вернул невалидный JSON. Ошибка: {e}\")\n            return []\n        except Exception as e:\n            logger.error(f\"Ошибка выполнения скрипта: {e}\")\n            return []\n\n    def update_cache(self, new_events, cache):\n        \"\"\"\n        Обновляет кеш с данными о событиях.\n        Ключ кеша – IP, а для каждого IP хранится массив событий.\n        Для каждого нового события (уникальность определяется по комбинации time и action)\n        если такого события ещё нет в кеше, оно добавляется и считается новым.\n        \"\"\"\n        diff = []  # список новых событий\n        current_day = datetime.now().strftime(\"%Y-%m-%d\")\n        for event in new_events:\n            if \"time\" not in event:\n                continue\n            if parse_event_day(event['time']) != current_day:\n                continue\n            ip = event['ip']\n            if ip not in cache:\n                cache[ip] = []\n            unique_key = f\"{event['time']}|{event['action']}\"\n            exists = any(f\"{e.get('time')}|{e.get('action')}\" == unique_key for e in cache[ip])\n            if not exists:\n                cache[ip].append(event)\n                diff.append(event)\n        return diff, cache\n\n    def produce_notification(self, diff):\n        \"\"\"\n        Генерирует уведомления для новых событий Fail2Ban в Markdown‑формате.\n        Для каждого нового события выводятся:\n          - Service\n          - Time\n          - IP\n          - Action\n        \"\"\"\n        if not diff:\n            return \"\"\n        message = \"\"\n        for event in diff:\n            message += f\"📢 Fail2Ban Alert\\n\"\n            message += \"```\\n\"\n            message += f\"Service: {event.get('service', 'unknown')}\\n\"\n            message += f\"Time   : {event.get('time', 'N/A')}\\n\"\n            message += f\"IP     : {event.get('ip', 'N/A')}\\n\"\n            message += f\"Action : {event.get('action', 'N/A')}\\n\"\n            message += \"```\\n\\n\"\n        return message\n\n    def run_monitoring(self):\n        \"\"\"\n        Основной процесс мониторинга:\n          1. Загружает кеш.\n          2. Получает новые события из скрипта.\n          3. Обновляет кеш, определяя новые события.\n          4. Сохраняет обновлённый кеш.\n          5. Генерирует уведомление, если обнаружены новые события.\n        \"\"\"\n        cache = self.load_cache()\n        new_events = self.process_script_output()\n        if not new_events:\n            #logger.error(\"Не удалось получить данные из скрипта или данные отсутствуют.\")\n            return 0\n        diff, cache = self.update_cache(new_events, cache)\n        self.save_cache(cache)\n        notification_message = self.produce_notification(diff)\n        if notification_message:\n            logger.info(\"Обнаружены новые события Fail2Ban:\\n\" + notification_message)\n            return notification_message\n        else:\n            logger.debug(\"Новых событий Fail2Ban не обнаружено.\")\n            return 0\n\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> subprocess</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> datetime </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> datetime</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">logger </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging.getLogger(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__name__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> parse_event_day</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(event_time):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Извлекает дату из event_time.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Ожидаемый формат: \"YYYY-MM-DD HH:MM:SS\", возвращает \"YYYY-MM-DD\".</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> event_time.split()[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">class</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Fail2BanMonitor</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> __init__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, cache_file, monitoring_script):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        :param cache_file: Путь к файлу кеша (например, \"fail2ban_cache.json\").</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        :param monitoring_script: Путь к shell‑скрипту (например, \"./fail2ban_parse.sh\").</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">               Скрипт должен выводить валидный JSON‑массив объектов с ключами:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                  - \"service\" (например, \"guacamole\")</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                  - \"time\"   (формат \"YYYY-MM-DD HH:MM:SS\")</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                  - \"ip\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                  - \"action\" (в данном случае всегда \"Ban\")</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.cache_file </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache_file</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.monitoring_script </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> monitoring_script</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> load_cache</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Загружает кеш из файла.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Структура кеша: {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">              \"ip1\": [event1, event2, ...],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">              \"ip2\": [...],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">              ...</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        }</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        При загрузке остаются только события текущего дня.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.exists(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.cache_file):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.cache_file, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"r\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json.load(f)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            current_day </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> datetime.now().strftime(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"%Y-%m-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            new_cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ip, events </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache.items():</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                filtered </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [event </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> event </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> events </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> event.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"time\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> parse_event_day(event[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"time\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> current_day]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> filtered:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    new_cache[ip] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> filtered</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_cache</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка при загрузке кеша: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> save_cache</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, cache):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Сохраняет кеш в файл.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.cache_file, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"w\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                json.dump(cache, f, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">indent</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">4</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка при сохранении кеша: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> process_script_output</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Выполняет shell‑скрипт для парсинга логов Fail2Ban.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Ожидается, что скрипт выводит валидный JSON‑массив объектов.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            result </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> subprocess.run(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                [</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.monitoring_script],</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                capture_output</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                check</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            )</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            output </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> result.stdout.strip()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json.loads(output)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json.JSONDecodeError </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Скрипт вернул невалидный JSON. Ошибка: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка выполнения скрипта: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> update_cache</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, new_events, cache):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Обновляет кеш с данными о событиях.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Ключ кеша – IP, а для каждого IP хранится массив событий.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Для каждого нового события (уникальность определяется по комбинации time и action)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        если такого события ещё нет в кеше, оно добавляется и считается новым.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        diff </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># список новых событий</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        current_day </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> datetime.now().strftime(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"%Y-%m-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> event </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_events:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"time\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> event:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                continue</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> parse_event_day(event[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'time'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> current_day:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ip </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> event[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'ip'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ip </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                cache[ip] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            unique_key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">event[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'time'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">|</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">event[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'action'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            exists </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> any</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'time'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">|</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'action'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ==</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> unique_key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache[ip])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> exists:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                cache[ip].append(event)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                diff.append(event)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> diff, cache</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> produce_notification</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, diff):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Генерирует уведомления для новых событий Fail2Ban в Markdown‑формате.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Для каждого нового события выводятся:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          - Service</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          - Time</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          - IP</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          - Action</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> diff:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> event </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> diff:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📢 Fail2Ban Alert</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"```</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Service: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">event.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'service'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'unknown'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Time   : </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">event.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'time'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'N/A'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"IP     : </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">event.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'ip'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'N/A'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Action : </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">event.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'action'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'N/A'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"```</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> message</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> run_monitoring</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Основной процесс мониторинга:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          1. Загружает кеш.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          2. Получает новые события из скрипта.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          3. Обновляет кеш, определяя новые события.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          4. Сохраняет обновлённый кеш.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          5. Генерирует уведомление, если обнаружены новые события.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.load_cache()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        new_events </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.process_script_output()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_events:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            #logger.error(\"Не удалось получить данные из скрипта или данные отсутствуют.\")</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        diff, cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.update_cache(new_events, cache)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.save_cache(cache)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        notification_message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.produce_notification(diff)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> notification_message:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.info(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Обнаружены новые события Fail2Ban:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> notification_message)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> notification_message</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.debug(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Новых событий Fail2Ban не обнаружено.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "manifest.yaml",
          "size": 2346,
          "content": "name: network\nversion: 0.3.8\ncategory: extra\ndescription: \"Сеть: сканер устройств, WireGuard/Guacamole/fail2ban-мониторы, MAC-блокировки, сниффер.\"\napi:\n  - network.scanner\n  - network.cache_file\n  - network.ignored_file\n  - network.devices_snapshot\n  - network.register_monitor\napi_methods:\n  - method: scanner\n    args: {}\n    returns: NetworkScanner — вендорный движок сканирования\n    desc: Доступ к сканеру (кеш устройств, мониторы wg/guac/f2b внутри)\n  - method: cache_file\n    args: {}\n    returns: str — путь networkscan_cache.json\n    desc: Путь кеша устройств (читает ui_web, пишет сканер)\n  - method: ignored_file\n    args: {}\n    returns: str — путь networkscan_ignored.json\n    desc: Путь игнор-листа сканера\n  - method: devices_snapshot\n    args: {}\n    returns: \"{total, online, devices[]} — обогащённый снимок\"\n    desc: Снимок устройств для панели мониторинга (свои кеши)\n  - method: register_monitor\n    args: \"{name, factory, interval_s, type, silent}\"\n    returns: \"True\"\n    desc: Плагин доп. источника для планировщика проверок (зовут network_*)\nevents_emitted:\n  - network.device_new\n  - network.device_reconnected\n  - network.device_disconnected\n  - network.device_ip_changed\nevents_subscribed: []\naudit_allow:\n  - JS_INNERHTML\nrights:\n  - name: nmap_manage\n    description: \"Сканер сети (GET/POST /api/admin/nmap*)\"\n  - name: receive_notifications_nmap\n    description: \"Уведомления сканера сети (новые/потерянные устройства)\"\n    all_ignore: true\n  - name: receive_notifications_nmap_edit\n    description: \"Право менять себе уведомления сканера сети\"\n    all_ignore: true\nuses_rights:\n  - view_net_status\nweb_routes:\n  - GET /api/network/devices\n  - GET /api/admin/nmap\n  - POST /api/admin/nmap/name\n  - POST /api/admin/nmap/ignore\n  - POST /api/network/nmap/delete\nshared_routes:\n  - /api/network/devices\n  - /api/admin/nmap\n  - /api/admin/nmap/name\n  - /api/admin/nmap/ignore\nconfig_schema: config.schema.json\nenv_example: .env.example\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">network</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">version</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0.3.8</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">category</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">extra</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Сеть: сканер устройств, WireGuard/Guacamole/fail2ban-мониторы, MAC-блокировки, сниффер.\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">network.scanner</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">network.cache_file</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">network.ignored_file</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">network.devices_snapshot</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">network.register_monitor</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">api_methods</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">scanner</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {}</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">NetworkScanner — вендорный движок сканирования</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Доступ к сканеру (кеш устройств, мониторы wg/guac/f2b внутри)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">cache_file</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {}</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">str — путь networkscan_cache.json</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Путь кеша устройств (читает ui_web, пишет сканер)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">ignored_file</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {}</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">str — путь networkscan_ignored.json</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Путь игнор-листа сканера</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">devices_snapshot</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {}</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{total, online, devices[]} — обогащённый снимок\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Снимок устройств для панели мониторинга (свои кеши)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">method</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">register_monitor</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    args</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"{name, factory, interval_s, type, silent}\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    returns</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"True\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    desc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Плагин доп. источника для планировщика проверок (зовут network_*)</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">events_emitted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">network.device_new</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">network.device_reconnected</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">network.device_disconnected</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">network.device_ip_changed</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">events_subscribed</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: []</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">audit_allow</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">JS_INNERHTML</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">rights</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">nmap_manage</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Сканер сети (GET/POST /api/admin/nmap*)\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">receive_notifications_nmap</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Уведомления сканера сети (новые/потерянные устройства)\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    all_ignore</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">true</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#22863A;--shiki-dark:#85E89D\">name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">receive_notifications_nmap_edit</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    description</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Право менять себе уведомления сканера сети\"</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">    all_ignore</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">true</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">uses_rights</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">view_net_status</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">web_routes</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/network/devices</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">GET /api/admin/nmap</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">POST /api/admin/nmap/name</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">POST /api/admin/nmap/ignore</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">POST /api/network/nmap/delete</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">shared_routes</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/api/network/devices</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/api/admin/nmap</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/api/admin/nmap/name</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  - </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/api/admin/nmap/ignore</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">config_schema</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">config.schema.json</span></span>\n<span class=\"line\"><span style=\"color:#22863A;--shiki-dark:#85E89D\">env_example</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.env.example</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "module.py",
          "size": 11619,
          "content": "\"\"\"network.Module — сканер сети + wireguard/guacamole/f2b-мониторы.\n\nСканер (NetworkScanner) собирается здесь и стартует фоном в start()\n(daemon-поток, как старый messengers/telegram.py). Отправка — через\nsender активного транспорта (берётся в start, транспорты уже подняты).\nПрава — RightsCheckerAdapter(ctx.rights). Кеши — data/cache/network\n(пишет сам; прод-кеш не трогаем). macblock TG-кнопки — legacy через ui_tg.\n\"\"\"\nimport logging\nimport os\nimport threading\n\nfrom core.base_module import BaseModule\nfrom core.legacy import RightsCheckerAdapter\n\nfrom . import config as pkg_config\n\nlogger = logging.getLogger(__name__)\n\n\nclass Module(BaseModule):\n    def setup(self, ctx) -> None:\n        mod_dir = os.path.dirname(os.path.abspath(__file__))\n        self.config = ctx.module_config(\"network\", mod_dir)\n        self.ctx = ctx\n        net_dir = os.path.join(ctx.data_dir, \"cache\", \"network\")\n\n        def _f(key, default):\n            return (self.config.get(key, \"\") or \"\").strip() or default\n\n        def _fp(key, default):\n            from core.paths import abs_data\n            return abs_data(ctx.data_dir,\n                            (self.config.get(key, \"\") or \"\").strip(),\n                            default)\n\n        files = {\n            \"MESSAGE_QUEUE_FILE\": _fp(\n                \"MESSAGE_QUEUE_FILE\", os.path.join(net_dir, \"message_queue.json\")),\n            \"CACHE_FILE_NETWORK\": _fp(\n                \"CACHE_FILE_NETWORK\", os.path.join(net_dir, \"networkscan_cache.json\")),\n            \"CACHE_FILE_WG\": _fp(\n                \"CACHE_FILE_WG\", os.path.join(net_dir, \"networkscan_cache_wg.json\")),\n            # wg/guacamole-скрипты живут в своих модулях (файлы на месте\n            # независимо от загрузки модулей).\n            \"SCRIPT_WG_DUMP\": os.path.join(\n                os.path.dirname(mod_dir), \"network_wireguard\", \"scripts\", \"wg_dump.sh\"),\n            \"SCRIPT_WG_PING\": os.path.join(\n                os.path.dirname(mod_dir), \"network_wireguard\", \"scripts\", \"wg_ping.sh\"),\n            \"SCRIPT_AG_LOGIN\": os.path.join(\n                os.path.dirname(mod_dir), \"network_guacamole\", \"scripts\", \"ag_login.sh\"),\n            \"SCRIPT_AG_CACHE\": _fp(\n                \"SCRIPT_AG_CACHE\", os.path.join(net_dir, \"guac_cache.json\")),\n            \"SCRIPT_F2B_CHECK\": os.path.join(mod_dir, \"scripts_net\", \"f2b_check.sh\"),\n            \"SCRIPT_F2B_CACHE\": _fp(\n                \"SCRIPT_F2B_CACHE\", os.path.join(net_dir, \"f2b_cache.json\")),\n            \"SCAN_NETWORK_IGNORED_MACS\": _fp(\n                \"SCAN_NETWORK_IGNORED_MACS\", os.path.join(net_dir, \"networkscan_ignored.json\")),\n        }\n        # Каталоги кешей обязан создать модуль (сканер пишет без makedirs).\n        for _d in (net_dir,):\n            try:\n                os.makedirs(_d, exist_ok=True)\n            except Exception:  # noqa: BLE001\n                pass\n        # install ДО импорта движков: macblock биндит значения при импорте.\n        pkg_config.install(dict(self.config.values), files)\n\n        try:\n            from . import scan as scan_mod\n        except ImportError as e:\n            from core.errors import ConfigError\n            raise ConfigError(\"network: движки требуют venv (telegram): %s\" % e)\n        self.scan_mod = scan_mod\n        self.rights_adapter = RightsCheckerAdapter(ctx.rights)\n        f2b_on = (self.config.get(\"F2B_ENABLED\", \"1\") or \"1\").strip() not in (\"0\", \"no\", \"false\")\n        # Цели уведомлений — единый ADMIN_IDS (модуль access, без дублей).\n        try:\n            notify_ids = ctx.api.call(\"access.admin_ids\")\n        except Exception:  # noqa: BLE001\n            notify_ids = []\n        self.scanner = scan_mod.NetworkScanner(\n            files[\"MESSAGE_QUEUE_FILE\"], files[\"CACHE_FILE_NETWORK\"],\n            files[\"CACHE_FILE_WG\"], files[\"SCRIPT_WG_DUMP\"], files[\"SCRIPT_WG_PING\"],\n            files[\"SCRIPT_AG_LOGIN\"], files[\"SCRIPT_AG_CACHE\"], f2b_on,\n            files[\"SCRIPT_F2B_CHECK\"], files[\"SCRIPT_F2B_CACHE\"],\n            notify_ids, self.rights_adapter, None,\n            pkg_config.SCAN_NETWORK_RANGE, pkg_config.CHECK_DURATION_NSCAN_LONG,\n            pkg_config.CHECK_DURATION_NSCAN_FAST, files[\"SCAN_NETWORK_IGNORED_MACS\"],\n            on_diff=self._on_diff)\n        self._thread = None\n        self._mon_thread = None\n        self._mon_stop = threading.Event()\n        self._monitor_registry = {}\n        self._files = files\n        self._f2b_on = f2b_on\n        ctx.api.register(\"network\", \"scanner\", lambda: self.scanner)\n        ctx.api.register(\"network\", \"cache_file\", lambda: files[\"CACHE_FILE_NETWORK\"])\n        ctx.api.register(\n            \"network\", \"ignored_file\", lambda: files[\"SCAN_NETWORK_IGNORED_MACS\"])\n        ctx.api.register(\"network\", \"register_monitor\", self._register_monitor)\n        from . import store as _store\n        ctx.api.register(\"network\", \"devices_snapshot\", _store.devices_snapshot)\n\n    def _on_diff(self, diff):\n        \"\"\"Хук сканера: diff -> события шины (по одному на устройство).\"\"\"\n        mapping = (\n            (\"new\", \"network.device_new\"),\n            (\"reconnected\", \"network.device_reconnected\"),\n            (\"disconnected\", \"network.device_disconnected\"),\n            (\"ip_changed\", \"network.device_ip_changed\"),\n        )\n        for section, event in mapping:\n            for mac, dev in ((diff or {}).get(section) or {}).items():\n                try:\n                    self.ctx.events.emit(event, {\n                        \"mac\": mac,\n                        \"ip\": dev.get(\"ip\"),\n                        \"old_ip\": dev.get(\"old_ip\"),\n                        \"name\": dev.get(\"name\"),\n                        \"name_custom\": dev.get(\"name_custom\") or \"\",\n                        \"type\": dev.get(\"type\"),\n                        \"ports\": list(dev.get(\"ports\") or []),\n                        \"first_appearance\": dev.get(\"first_appearance\"),\n                        \"last_appearance\": dev.get(\"last_appearance\"),\n                        \"last_disconnection\": dev.get(\"last_disconnection\"),\n                        \"connections_total\": dev.get(\"connections_total\"),\n                    })\n                except Exception:  # noqa: BLE001\n                    logger.exception(\"network emit %s failed\", event)\n\n    def _register_monitor(self, name, spec):\n        \"\"\"Плагин доп. источника: {factory, interval_s, type, silent}.\n\n        factory() -> объект с run_monitoring() -> str. Вызывают network_*\n        модули в своём setup; интервалы у каждого свои (свой .env).\n        \"\"\"\n        if not name or not isinstance(spec, dict):\n            raise ValueError(\"register_monitor: нужно имя и spec\")\n        factory = spec.get(\"factory\")\n        if not callable(factory):\n            raise ValueError(\"register_monitor %s: нет factory\" % name)\n        try:\n            interval = int(spec.get(\"interval_s\", 60) or 60)\n        except (TypeError, ValueError):\n            raise ValueError(\"register_monitor %s: interval_s — число\" % name)\n        interval = max(5, min(86400, interval))\n        notif_type = str(spec.get(\"type\") or name)\n        self._monitor_registry[str(name)] = {\n            \"factory\": factory, \"interval\": interval,\n            \"type\": notif_type, \"silent\": bool(spec.get(\"silent\", False)),\n        }\n        logger.info(\"network: монитор %s каждые %s с\", name, interval)\n        return True\n\n    def start(self) -> None:\n        try:\n            self.scanner.bot = self.ctx.api.call(\"transport_telegram.sender\")\n        except Exception as e:  # noqa: BLE001\n            logger.warning(\"network: нет sender: %s\", e)\n        # Штатный f2b-монитор сканера + зарегистрированные network_*.\n        from . import fail2ban_mon as _f2b\n        monitors = {\"f2b\": {\n            \"monitor\": _f2b.Fail2BanMonitor(\n                self._files[\"SCRIPT_F2B_CACHE\"],\n                self._files[\"SCRIPT_F2B_CHECK\"],\n            ) if self._f2b_on else None,\n            \"interval\": int(self.config.get(\"CHECK_DURATION_NSCAN_FAST\", \"5\") or 5),\n            \"type\": \"f2b\", \"silent\": True}}\n        for name, spec in self._monitor_registry.items():\n            try:\n                monitors[name] = {\n                    \"monitor\": spec[\"factory\"](),\n                    \"interval\": spec[\"interval\"],\n                    \"type\": spec[\"type\"], \"silent\": spec[\"silent\"]}\n            except Exception as e:  # noqa: BLE001\n                logger.warning(\"network: монитор %s не поднялся: %s\", name, e)\n        self._monitors = {n: m for n, m in monitors.items()\n                          if m.get(\"monitor\") is not None}\n        self._mon_stop.clear()\n        self._thread = threading.Thread(target=self._run_scan, daemon=True,\n                                        name=\"net-scanner\")\n        self._thread.start()\n        self._mon_thread = threading.Thread(target=self._run_monitors, daemon=True,\n                                            name=\"net-monitors\")\n        self._mon_thread.start()\n\n    def _run_monitors(self):\n        import time as _time\n        due = {}\n        tick = 5\n        try:\n            tick = max(1, min([m[\"interval\"] for m in self._monitors.values()] or [5]))\n        except Exception:  # noqa: BLE001\n            pass\n        while not self._mon_stop.wait(tick):\n            now = _time.time()\n            for name, spec in list(self._monitors.items()):\n                if now < due.get(name, 0):\n                    continue\n                due[name] = now + spec[\"interval\"]\n                try:\n                    msg = spec[\"monitor\"].run_monitoring()\n                except Exception as e:  # noqa: BLE001\n                    logger.warning(\"network monitor %s: %s\", name, type(e).__name__)\n                    continue\n                try:\n                    if msg:\n                        self.scanner.send_notification(\n                            msg, notif_type=spec[\"type\"], disable_notif=spec[\"silent\"])\n                    else:\n                        with self.scanner._queue_lock:\n                            queued = [m for m in self.scanner.load_message_queue()\n                                      if m.get(\"type\") == spec[\"type\"]]\n                        if queued:\n                            self.scanner.send_notification(\n                                \"\", notif_type=spec[\"type\"], disable_notif=spec[\"silent\"])\n                except Exception as e:  # noqa: BLE001\n                    logger.warning(\"network notify %s: %s\", name, e)\n\n    def stop(self) -> None:\n        self._mon_stop.set()\n        for t in (self._thread, self._mon_thread):\n            if t is not None:\n                try:\n                    t.join(timeout=10)\n                except Exception:  # noqa: BLE001\n                    pass\n\n    def _run_scan(self):\n        try:\n            self.scanner.start_scan()\n        except Exception as e:  # noqa: BLE001\n            logger.error(\"network scan: %s\", e)\n\n    def health(self) -> dict:\n        alive = self._thread.is_alive() if self._thread else False\n        return {\"ok\": self.scanner is not None, \"module\": self.name,\n                \"scanner\": alive}\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"network.Module — сканер сети + wireguard/guacamole/f2b-мониторы.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Сканер (NetworkScanner) собирается здесь и стартует фоном в start()</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(daemon-поток, как старый messengers/telegram.py). Отправка — через</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">sender активного транспорта (берётся в start, транспорты уже подняты).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Права — RightsCheckerAdapter(ctx.rights). Кеши — data/cache/network</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(пишет сам; прод-кеш не трогаем). macblock TG-кнопки — legacy через ui_tg.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.base_module </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> BaseModule</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.legacy </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> RightsCheckerAdapter</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pkg_config</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">logger </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging.getLogger(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__name__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">class</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Module</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">BaseModule</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> setup</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, ctx) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mod_dir </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.dirname(os.path.abspath(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__file__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.module_config(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, mod_dir)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        net_dir </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.join(ctx.data_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cache\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _f</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(key, default):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.get(key, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> default</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _fp</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(key, default):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.paths </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> abs_data</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> abs_data(ctx.data_dir,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                            (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.get(key, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip(),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                            default)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        files </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"MESSAGE_QUEUE_FILE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _fp(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"MESSAGE_QUEUE_FILE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, os.path.join(net_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"message_queue.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"CACHE_FILE_NETWORK\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _fp(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"CACHE_FILE_NETWORK\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, os.path.join(net_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"networkscan_cache.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"CACHE_FILE_WG\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _fp(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"CACHE_FILE_WG\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, os.path.join(net_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"networkscan_cache_wg.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # wg/guacamole-скрипты живут в своих модулях (файлы на месте</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # независимо от загрузки модулей).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"SCRIPT_WG_DUMP\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: os.path.join(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                os.path.dirname(mod_dir), </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network_wireguard\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"wg_dump.sh\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"SCRIPT_WG_PING\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: os.path.join(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                os.path.dirname(mod_dir), </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network_wireguard\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"wg_ping.sh\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"SCRIPT_AG_LOGIN\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: os.path.join(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                os.path.dirname(mod_dir), </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network_guacamole\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scripts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ag_login.sh\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"SCRIPT_AG_CACHE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _fp(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"SCRIPT_AG_CACHE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, os.path.join(net_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"guac_cache.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"SCRIPT_F2B_CHECK\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: os.path.join(mod_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scripts_net\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"f2b_check.sh\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"SCRIPT_F2B_CACHE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _fp(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"SCRIPT_F2B_CACHE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, os.path.join(net_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"f2b_cache.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"SCAN_NETWORK_IGNORED_MACS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _fp(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"SCAN_NETWORK_IGNORED_MACS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, os.path.join(net_dir, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"networkscan_ignored.json\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Каталоги кешей обязан создать модуль (сканер пишет без makedirs).</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (net_dir,):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                os.makedirs(_d, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">exist_ok</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                pass</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # install ДО импорта движков: macblock биндит значения при импорте.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        pkg_config.install(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.values), files)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> scan </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> scan_mod</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ImportError</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.errors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ConfigError</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ConfigError(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network: движки требуют venv (telegram): </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.scan_mod </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> scan_mod</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.rights_adapter </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> RightsCheckerAdapter(ctx.rights)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        f2b_on </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"F2B_ENABLED\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"1\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"1\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"0\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"no\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"false\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Цели уведомлений — единый ADMIN_IDS (модуль access, без дублей).</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            notify_ids </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"access.admin_ids\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            notify_ids </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.scanner </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> scan_mod.NetworkScanner(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            files[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MESSAGE_QUEUE_FILE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], files[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"CACHE_FILE_NETWORK\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            files[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"CACHE_FILE_WG\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], files[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SCRIPT_WG_DUMP\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], files[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SCRIPT_WG_PING\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            files[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SCRIPT_AG_LOGIN\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], files[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SCRIPT_AG_CACHE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], f2b_on,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            files[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SCRIPT_F2B_CHECK\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], files[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SCRIPT_F2B_CACHE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            notify_ids, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.rights_adapter, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            pkg_config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">SCAN_NETWORK_RANGE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, pkg_config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">CHECK_DURATION_NSCAN_LONG</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            pkg_config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">CHECK_DURATION_NSCAN_FAST</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, files[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SCAN_NETWORK_IGNORED_MACS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            on_diff</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._on_diff)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._thread </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._mon_thread </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._mon_stop </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading.Event()</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._monitor_registry </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._files </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> files</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._f2b_on </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f2b_on</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"scanner\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">lambda</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.scanner)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"cache_file\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">lambda</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: files[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"CACHE_FILE_NETWORK\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"network\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ignored_file\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">lambda</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: files[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SCAN_NETWORK_IGNORED_MACS\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"register_monitor\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._register_monitor)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> store </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _store</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ctx.api.register(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"devices_snapshot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, _store.devices_snapshot)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _on_diff</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, diff):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Хук сканера: diff -> события шины (по одному на устройство).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mapping </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"new\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network.device_new\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"reconnected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network.device_reconnected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"disconnected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network.device_disconnected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip_changed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network.device_ip_changed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        )</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> section, event </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mapping:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac, dev </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ((diff </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}).get(section) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}).items():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx.events.emit(event, {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                        \"mac\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: mac,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                        \"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                        \"old_ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"old_ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                        \"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                        \"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                        \"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                        \"ports\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">list</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ports\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                        \"first_appearance\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"first_appearance\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                        \"last_appearance\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"last_appearance\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                        \"last_disconnection\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"last_disconnection\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                        \"connections_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connections_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    })</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    logger.exception(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network emit </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> failed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, event)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _register_monitor</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, name, spec):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Плагин доп. источника: {factory, interval_s, type, silent}.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        factory() -> объект с run_monitoring() -> str. Вызывают network_*</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        модули в своём setup; интервалы у каждого свои (свой .env).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(spec, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"register_monitor: нужно имя и spec\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        factory </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> spec.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"factory\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> callable</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(factory):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"register_monitor </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: нет factory\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            interval </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(spec.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"interval_s\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">60</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 60</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TypeError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"register_monitor </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: interval_s — число\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        interval </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> max</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">min</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">86400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, interval))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        notif_type </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(spec.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._monitor_registry[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name)] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"factory\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: factory, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"interval\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: interval,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: notif_type, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"silent\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(spec.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"silent\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        logger.info(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network: монитор </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> каждые </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> с\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, name, interval)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> start</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.scanner.bot </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"transport_telegram.sender\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.warning(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network: нет sender: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, e)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Штатный f2b-монитор сканера + зарегистрированные network_*.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fail2ban_mon </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _f2b</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        monitors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"f2b\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"monitor\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _f2b.Fail2BanMonitor(</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._files[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SCRIPT_F2B_CACHE\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._files[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"SCRIPT_F2B_CHECK\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._f2b_on </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"interval\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.config.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"CHECK_DURATION_NSCAN_FAST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"5\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"f2b\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"silent\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name, spec </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._monitor_registry.items():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                monitors[name] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"monitor\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: spec[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"factory\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">](),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"interval\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: spec[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"interval\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: spec[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"silent\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: spec[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"silent\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                logger.warning(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network: монитор </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> не поднялся: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, name, e)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._monitors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {n: m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> n, m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> monitors.items()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                          if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"monitor\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._mon_stop.clear()</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._thread </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading.Thread(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">target</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._run_scan, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">daemon</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                                        name</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net-scanner\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._thread.start()</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._mon_thread </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading.Thread(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">target</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._run_monitors, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">daemon</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                                            name</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net-monitors\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._mon_thread.start()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _run_monitors</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> time </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _time</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        due </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        tick </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 5</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            tick </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> max</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">min</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">([m[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"interval\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._monitors.values()] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        while</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._mon_stop.wait(tick):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _time.time()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name, spec </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> list</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._monitors.items()):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> due.get(name, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                due[name] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> spec[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"interval\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> spec[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"monitor\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].run_monitoring()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    logger.warning(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network monitor </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, name, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">type</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e).</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__name__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    continue</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.scanner.send_notification(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                            msg, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">notif_type</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">spec[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">disable_notif</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">spec[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"silent\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.scanner._queue_lock:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                            queued </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.scanner.load_message_queue()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                                      if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> spec[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> queued:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.scanner.send_notification(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                                \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">notif_type</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">spec[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">disable_notif</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">spec[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"silent\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    logger.warning(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network notify </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, name, e)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> stop</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._mon_stop.set()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> t </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._thread, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._mon_thread):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> t </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    t.join(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">timeout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">10</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    pass</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _run_scan</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.scanner.start_scan()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network scan: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, e)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> health</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        alive </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._thread.is_alive() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._thread </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.scanner </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"module\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.name,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"scanner\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: alive}</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "scan.py",
          "size": 39352,
          "content": "import subprocess\nimport json\nimport time\nimport logging\nimport os\nfrom datetime import datetime, timedelta\nimport threading\nfrom threading import Thread\nfrom telegram import InlineKeyboardButton, InlineKeyboardMarkup\nfrom .fail2ban_mon import Fail2BanMonitor  # BOTMOD: штатный монитор сканера\nimport re\nimport copy\nfrom . import config  # BOTMOD: было import config\n\nlogger = logging.getLogger(__name__)\n\ndef escape_markdown_v2(text):\n    \"\"\"Экранирует специальные символы для MarkdownV2.\n\n    Содержимое fenced-блоков кода (между ```) не экранируется,\n    иначе символы внутри блоков отображались бы с лишними обратными слэшами.\n    \"\"\"\n    in_code = False\n    result = []\n    for line in text.split(\"\\n\"):\n        if line.strip().startswith(\"```\"):\n            in_code = not in_code\n            result.append(line)\n            continue\n        if in_code:\n            result.append(line)\n        else:\n            result.append(re.sub(r'([_*\\[\\]()~>#+\\-=|{}.!])', r'\\\\\\1', line))\n    return \"\\n\".join(result)\n\ndef format_duration(seconds):\n    return str(timedelta(seconds=seconds))\n\ndef is_host_reachable(ip):\n    \"\"\"\n    Проверяет, доступен ли хост через пинг.\n    Возвращает True, если хотя бы один пакет успешно дошёл.\n    \"\"\"\n    try:\n        # Подробный пинг (10 пакетов) важен для проверки \"отключаемых\" устройств:\n        # мобильные устройства (телефоны) могут не всегда отвечать на nmap-скан,\n        # поэтому чтобы не считать ложное отключение, даём им достаточно попыток.\n        result = subprocess.run(\n            [\"ping\", \"-c\", \"10\", ip],\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            timeout=20\n        )\n        # Успех, если код возврата 0\n        return result.returncode == 0\n    except Exception as e:\n        logger.error(f\"Ошибка проверки пинга для {ip}: {e}\")\n        return False\n\n\ndef _extract_new_device_macs(text):\n    \"\"\"Извлекает MAC-адреса новых устройств (секции, начинающиеся с 🚨).\"\"\"\n    out = []\n    for m in re.finditer(r\"\\U0001F6A8.{0,300}?MAC:\\s*((?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2})\",\n                         text, re.S):\n        mac = m.group(1).upper().replace(\"-\", \":\")\n        if mac not in out:\n            out.append(mac)\n    return out\n\n\n# Лимит Telegram — 4096 символов, но MarkdownV2-экранирование раздувает\n# текст (каждый спецсимвол + обратный слэш). Режем с запасом: 3000 сырых.\nTG_CHUNK_LIMIT = 3000\n\n\ndef split_tg_message(text, limit=TG_CHUNK_LIMIT):\n    \"\"\"Режет длинное уведомление на части по границам блоков (пустая строка).\n\n    Блоки устройств (~150 символов) никогда не рвутся пополам; одиночный\n    сверхдлинный блок без '\\n\\n' режется жёстко по лимиту.\n    \"\"\"\n    text = text or \"\"\n    if len(text) <= limit:\n        return [text]\n    parts, cur, cur_len = [], [], 0\n    for block in text.split(\"\\n\\n\"):\n        b = block.strip(\"\\n\")\n        if not b:\n            continue\n        if len(b) > limit:\n            # аварийный случай: сам блок больше лимита — режем жёстко\n            if cur:\n                parts.append(\"\\n\\n\".join(cur))\n                cur, cur_len = [], 0\n            for i in range(0, len(b), limit):\n                parts.append(b[i:i + limit])\n            continue\n        if cur and cur_len + len(b) + 2 > limit:\n            parts.append(\"\\n\\n\".join(cur))\n            cur, cur_len = [], 0\n        cur.append(b)\n        cur_len += len(b) + 2\n    if cur:\n        parts.append(\"\\n\\n\".join(cur))\n    return parts or [text]\n\n\nclass NetworkScanner:\n    def __init__(self, message_queue, cache_file, wg_cache_file, monitoring_script, ping_script,\n                 ag_script, ag_cache, f2b_enabled, f2b_script, f2b_cache, admin_ids, rights_checker_class, sender,\n                 network_range, long_check_interval, fast_check_interval, mac_filter_file,\n                 on_diff=None):\n        self.message_queue_file = message_queue  # путь к файлу очереди сообщений\n        self._queue_lock = threading.Lock()  # защищает очередь сообщений от гонок потоков\n        self.cache_file = cache_file\n        self.admin_ids = admin_ids\n        self.rights_checker = rights_checker_class\n        # sender — шлюз messengers.sender.GatewaySender (активный транспорт).\n        self.bot = sender\n        self.network_range = network_range\n        self.long_check_interval = long_check_interval  # интервал для долгих проверок (сканирование сети)\n        self.fast_check_interval = fast_check_interval  # интервал для быстрых проверок (мониторинг доп. источников)\n\n        self.mac_filter_file = mac_filter_file\n        self.excluded_macs = self.load_mac_filter()\n\n        self.wg_monitor = None  # BOTMOD: мониторы доп. источников вешает модуль (register_monitor)\n        self.ag_monitor = None\n        self.f2b_monitor = Fail2BanMonitor(f2b_cache, f2b_script)\n        # on_diff(diff) — хук сырых diff скана для шины событий (ставит модуль).\n        self._on_diff = on_diff\n\n    def load_cache(self):\n        \"\"\"Загружает кэш из JSON-файла\"\"\"\n        if not os.path.exists(self.cache_file):\n            return {}\n        with open(self.cache_file, 'r') as f:\n            try:\n                return json.load(f)\n            except Exception as e:\n                logger.error(f\"Ошибка загрузки кэша: {e}\")\n                return {}\n\n    def save_cache(self, cache):\n        \"\"\"Сохраняет кэш в JSON-файл\"\"\"\n        with open(self.cache_file, 'w') as f:\n            json.dump(cache, f, indent=4)\n\n    def load_mac_filter(self):\n        \"\"\"Загружает список MAC-адресов, которые нужно исключить, из JSON файла.\"\"\"\n        if not os.path.exists(self.mac_filter_file):\n            return []\n        try:\n            with open(self.mac_filter_file, 'r') as f:\n                mac_list = json.load(f)\n            if not isinstance(mac_list, list):\n                logger.error(\"Формат JSON для фильтра MAC должен быть списком.\")\n                return []\n            # Приводим все MAC-адреса к верхнему регистру для корректного сравнения\n            return [mac.upper() for mac in mac_list]\n        except Exception as e:\n            logger.error(f\"Ошибка загрузки MAC фильтра: {e}\")\n            return []\n\n\n    def perform_network_scan(self):\n        \"\"\"\n        Выполняет sweep-скан сети с помощью nmap:\n        nmap -sn -T4 --privileged <NETWORK_RANGE>\n        Возвращает словарь устройств.\n        \"\"\"\n        cmd = ['nmap', '-sn', '-T4', '--privileged', self.network_range]\n        try:\n            result = subprocess.run(cmd, capture_output=True, text=True, check=True)\n        except subprocess.CalledProcessError as e:\n            logger.error(f\"Ошибка сканирования сети: {e}\")\n            return {}\n        devices = {}\n        current_device = None\n        self.excluded_macs = self.load_mac_filter()\n        for line in result.stdout.splitlines():\n            line = line.strip()\n            if line.startswith(\"Nmap scan report for\"):\n                if '(' in line and ')' in line:\n                    ip = line.split('(')[-1].split(')')[0]\n                    hostname = line.split('(')[0].split()[-1]\n                else:\n                    ip = line.split()[-1]\n                    hostname = \"Unknown\"\n                current_device = {\n                    \"ip\": ip,\n                    \"name\": hostname,\n                    \"mac\": None,\n                    \"ports\": [],\n                    \"type\": \"Unknown\"\n                }\n            elif line.startswith(\"MAC Address:\") and current_device is not None:\n                parts = line.split()\n                mac = parts[2].upper()\n                if mac in self.excluded_macs:\n                    current_device = None\n                    continue\n                current_device[\"mac\"] = mac\n                if \"(\" in line and \")\" in line:\n                    vendor = line.split(\"(\")[-1].split(\")\")[0]\n                    current_device[\"type\"] = vendor.strip()\n                devices[current_device[\"mac\"]] = current_device\n                current_device = None\n        return devices\n\n    def perform_port_scan(self, ip):\n        \"\"\"\n        Выполняет полный портскан для указанного IP:\n        nmap -p- --open -T4 --privileged <ip>\n        Возвращает список открытых портов.\n        \"\"\"\n        cmd = ['nmap', '-p-', '--open', '-T5', ip]\n        try:\n            result = subprocess.run(cmd, capture_output=True, text=True, check=True)\n        except subprocess.CalledProcessError as e:\n            logger.error(f\"Ошибка сканирования портов для {ip}: {e}\")\n            return []\n        ports = []\n        for line in result.stdout.splitlines():\n            if \"/tcp\" in line and \"open\" in line:\n                port = line.split(\"/\")[0]\n                ports.append(port)\n        return ports if ports else [\"Нет открытых портов\"]\n\n    def add_ignored_mac(self, mac):\n        mac = mac.upper()\n        cache = self.load_cache()\n        if mac not in cache:\n             return False, \"MAC адрес не найден в кэше сети.\"\n        ignored = self.load_mac_filter()\n        if mac in ignored:\n             return False, \"MAC адрес уже присутствует в игнор-листе.\"\n        ignored.append(mac)\n        try:\n             with open(self.mac_filter_file, \"w\", encoding=\"utf-8\") as f:\n                  json.dump(ignored, f, indent=4)\n             return True, \"MAC адрес успешно добавлен в игнор-лист.\"\n        except Exception as e:\n             return False, f\"Ошибка сохранения: {e}\"\n\n    def remove_ignored_mac(self, mac):\n        mac = mac.upper()\n        ignored = self.load_mac_filter()\n        if mac not in ignored:\n             return False, \"MAC адрес отсутствует в игнор-листе.\"\n        ignored.remove(mac)\n        try:\n             with open(self.mac_filter_file, \"w\", encoding=\"utf-8\") as f:\n                  json.dump(ignored, f, indent=4)\n             return True, \"MAC адрес успешно удалён из игнор-листа.\"\n        except Exception as e:\n             return False, f\"Ошибка сохранения: {e}\"\n\n    def _ensure_stats(self, dev, now):\n        \"\"\"\n        Гарантирует наличие полей статистики у устройства и выполняет сброс\n        суточных счётчиков при смене календарного дня.\n        \"\"\"\n        if \"connections_total\" not in dev:\n            dev[\"connections_total\"] = 0\n        if \"connections_today\" not in dev:\n            dev[\"connections_today\"] = 0\n        if \"online_seconds_total\" not in dev:\n            dev[\"online_seconds_total\"] = 0\n        if \"online_seconds_today\" not in dev:\n            dev[\"online_seconds_today\"] = 0\n        if \"online_start_ts\" not in dev:\n            dev[\"online_start_ts\"] = None\n        today = datetime.now().strftime(\"%Y-%m-%d\")\n        if dev.get(\"stats_date\") != today:\n            dev[\"stats_date\"] = today\n            dev[\"connections_today\"] = 0\n            dev[\"online_seconds_today\"] = 0\n        if dev.get(\"connected\") and dev.get(\"online_start_ts\") is None:\n            dev[\"online_start_ts\"] = now\n        return dev\n\n    def update_cache_and_diff(self, scanned_devices):\n        \"\"\"\n        Сравнивает данные текущего сканирования с кэшем, обновляет статус `connected`\n        и возвращает diff, при этом пытаясь сохранить изменения (например, индивидуальное\n        кастомное имя) внесённые параллельно другим процессом.\n        \"\"\"\n        # Шаг 1. Загружаем исходный кэш и делаем глубокую копию для редактирования.\n        baseline_cache = self.load_cache()         # эталонная версия\n        editable_cache = copy.deepcopy(baseline_cache)  # версия для редактирования\n        now = time.time()\n        diff = {\"new\": {}, \"reconnected\": {}, \"disconnected\": {}, \"ip_changed\": {}}\n\n        # Шаг 2. Обработка данных сканирования (scanned_devices)\n        for mac, dev in scanned_devices.items():\n            if mac not in editable_cache:\n                dev[\"first_appearance\"] = now\n                dev[\"last_appearance\"] = now\n                dev[\"last_disconnection\"] = None\n                dev[\"ports\"] = self.perform_port_scan(dev[\"ip\"])\n                dev[\"connected\"] = True\n                dev[\"name_custom\"] = \"\"\n                dev[\"miss_count\"] = 0\n                dev[\"stats_date\"] = datetime.now().strftime(\"%Y-%m-%d\")\n                dev[\"connections_total\"] = 1\n                dev[\"connections_today\"] = 1\n                dev[\"online_seconds_total\"] = 0\n                dev[\"online_seconds_today\"] = 0\n                dev[\"online_start_ts\"] = now\n                editable_cache[mac] = dev\n                diff[\"new\"][mac] = dev\n            else:\n                cached = editable_cache[mac]\n                # Сохраняем ранее установленные значения, в том числе кастомное имя.\n                dev[\"first_appearance\"] = cached.get(\"first_appearance\", now)\n                dev[\"last_disconnection\"] = cached.get(\"last_disconnection\")\n                dev[\"name_custom\"] = cached.get(\"name_custom\", \"\")\n                # Устройство видно в сканировании — сбрасываем счётчик пропусков.\n                dev[\"miss_count\"] = 0\n                # Переносим накопленную статистику из кэша и делаем суточный сброс при необходимости.\n                dev[\"connections_total\"] = cached.get(\"connections_total\", 0)\n                dev[\"connections_today\"] = cached.get(\"connections_today\", 0)\n                dev[\"online_seconds_total\"] = cached.get(\"online_seconds_total\", 0)\n                dev[\"online_seconds_today\"] = cached.get(\"online_seconds_today\", 0)\n                dev[\"online_start_ts\"] = cached.get(\"online_start_ts\")\n                dev[\"stats_date\"] = cached.get(\"stats_date\")\n                self._ensure_stats(dev, now)\n                # Определяем смену IP-адреса (например, при обновлении DHCP-аренды).\n                old_ip = cached.get(\"ip\")\n                if old_ip and old_ip != dev.get(\"ip\"):\n                    ip_info = copy.deepcopy(dev)\n                    ip_info[\"old_ip\"] = old_ip\n                    diff[\"ip_changed\"][mac] = ip_info\n                if not cached.get(\"connected\"):\n                    dev[\"last_appearance\"] = now\n                    dev[\"ports\"] = self.perform_port_scan(dev[\"ip\"])\n                    dev[\"connected\"] = True\n                    dev[\"connections_total\"] += 1\n                    dev[\"connections_today\"] += 1\n                    dev[\"online_start_ts\"] = now\n                    editable_cache[mac] = dev\n                    diff[\"reconnected\"][mac] = dev\n                else:\n                    dev[\"last_appearance\"] = cached.get(\"last_appearance\", now)\n                    dev[\"ports\"] = cached.get(\"ports\", [])\n                    dev[\"connected\"] = True\n                    editable_cache[mac] = dev\n\n        # Обработка устройств, которых нет в текущем сканировании.\n        # Отключённым считаем только после DEBOUNCE_COUNT пропусков подряд\n        # (нет в nmap И не отвечает на пинг), чтобы не срабатывать на ложные пропадания.\n        for mac in list(editable_cache.keys()):\n            if mac not in scanned_devices and editable_cache[mac].get(\"connected\"):\n                dev = editable_cache[mac]\n                self._ensure_stats(dev, now)\n                ip = dev.get(\"ip\")\n                reachable = bool(ip) and is_host_reachable(ip)\n                if reachable:\n                    dev[\"miss_count\"] = 0\n                else:\n                    miss = dev.get(\"miss_count\", 0) + 1\n                    dev[\"miss_count\"] = miss\n                    if miss >= config.DEBOUNCE_COUNT:\n                        # Начисляем время онлайн и закрываем интервал.\n                        online_start = dev.get(\"online_start_ts\")\n                        if online_start:\n                            dev[\"online_seconds_total\"] = dev.get(\"online_seconds_total\", 0) + (now - online_start)\n                            today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp()\n                            dev[\"online_seconds_today\"] = dev.get(\"online_seconds_today\", 0) + (now - max(online_start, today_start))\n                            dev[\"online_start_ts\"] = None\n                        dev[\"last_disconnection\"] = now\n                        dev[\"connected\"] = False\n                        diff[\"disconnected\"][mac] = dev\n\n        # Шаг 3. Прежде чем сохранить, пытаемся сохранить изменения, внесённые параллельно.\n        current_cache = self.load_cache()  # Текущая версия кэша из файла\n        if current_cache != baseline_cache:\n            # Для каждого MAC-адреса проверяем, если поле name_custom изменилось в current_cache,\n            # то обновляем его в editable_cache.\n            for mac, current_dev in current_cache.items():\n                if mac in baseline_cache:\n                    # Если значение name_custom в текущем кэше отличается от эталонного,\n                    # то используем его в editable_cache.\n                    baseline_name = baseline_cache[mac].get(\"name_custom\", \"\")\n                    current_name = current_dev.get(\"name_custom\", \"\")\n                    if current_name != baseline_name:\n                        # Обновляем кастомное имя в редактируемом кэше\n                        if mac in editable_cache:\n                            editable_cache[mac][\"name_custom\"] = current_name\n                else:\n                    # Если новое устройство появилось в current_cache, добавляем его в editable_cache.\n                    self._ensure_stats(current_dev, now)\n                    editable_cache[mac] = current_dev\n\n        # Шаг 4. Сохраняем полностью обновлённый кэш и возвращаем сформированный diff.\n        self.save_cache(editable_cache)\n        return diff\n\n\n    def produce_notification(self, diff):\n        \"\"\"Формирует сообщение об изменениях в сети.\"\"\"\n        message = \"\"\n        # Новые устройства\n        if diff.get(\"new\"):\n            for mac, dev in diff[\"new\"].items():\n                rule = \"new\"\n                ip = dev.get(\"ip\", \"Unknown\")\n                message += f\"🚨 {ip} ({rule}):\\n\"\n                message += \"```\\n\"\n                message += f\"MAC: {mac}\\n\"\n                message += f\"Name: {dev.get('name', 'Unknown')}\\n\"\n                message += f\"Ports: {', '.join(dev.get('ports', []))}\\n\"\n                message += f\"Type: {dev.get('type', 'Unknown')}\\n\"\n                message += f\"First appearance: {datetime.fromtimestamp(dev.get('first_appearance')).strftime('%Y-%m-%d %H:%M:%S')}\\n\"\n                message += \"```\\n\\n\"\n        # Повторное подключение\n        if diff.get(\"reconnected\"):\n            for mac, dev in diff[\"reconnected\"].items():\n                last_disconnection = dev.get(\"last_disconnection\")\n                name_custom = dev.get(\"name_custom\", \"\")\n                ip = dev.get(\"ip\", \"Unknown\")\n                if last_disconnection is not None:\n                    duration = format_duration(int(time.time() - last_disconnection))\n                    rule = f\"connected after {duration} offline\"\n                else:\n                    rule = \"connected\"\n                if name_custom:\n                    message += f\"🔄 {name_custom} ({rule}):\\n\"\n                else:\n                    message += f\"🔄 {ip} ({rule}):\\n\"\n                message += \"```\\n\"\n                if name_custom:\n                    message += f\"IP: {ip}\\n\"\n                message += f\"MAC: {mac}\\n\"\n                message += f\"Name: {dev.get('name', 'Unknown')}\\n\"\n                message += f\"Ports: {', '.join(dev.get('ports', []))}\\n\"\n                message += f\"Type: {dev.get('type', 'Unknown')}\\n\"\n                message += f\"First appearance: {datetime.fromtimestamp(dev.get('first_appearance')).strftime('%Y-%m-%d %H:%M:%S')}\\n\"\n                message += \"```\\n\\n\"\n        # Отключившиеся устройства\n        if diff.get(\"disconnected\"):\n            for mac, dev in diff[\"disconnected\"].items():\n                last_appearance = dev.get(\"last_appearance\")\n                name_custom = dev.get(\"name_custom\", \"\")\n                ip = dev.get(\"ip\", \"Unknown\")\n                if last_appearance is not None:\n                    duration = format_duration(int(time.time() - last_appearance))\n                    rule = f\"disconnected after {duration} online\"\n                else:\n                    rule = \"disconnected\"\n                if name_custom:\n                    message += f\"🔌 {name_custom} ({rule}):\\n\"\n                else:\n                    message += f\"🔌 {ip} ({rule}):\\n\"\n                message += \"```\\n\"\n                if name_custom:\n                    message += f\"IP: {ip}\\n\"\n                message += f\"MAC: {mac}\\n\"\n                message += f\"Name: {dev.get('name', 'Unknown')}\\n\"\n                message += f\"Ports: {', '.join(dev.get('ports', []))}\\n\"\n                message += f\"Type: {dev.get('type', 'Unknown')}\\n\"\n                message += f\"First appearance: {datetime.fromtimestamp(dev.get('first_appearance')).strftime('%Y-%m-%d %H:%M:%S')}\\n\"\n                message += \"```\\n\\n\"\n        # Смена IP-адреса\n        if diff.get(\"ip_changed\"):\n            for mac, dev in diff[\"ip_changed\"].items():\n                name_custom = dev.get(\"name_custom\", \"\")\n                ip = dev.get(\"ip\", \"Unknown\")\n                old_ip = dev.get(\"old_ip\", \"Unknown\")\n                title = name_custom if name_custom else ip\n                message += f\"📍 {title} сменил IP:\\n\"\n                message += f\"  • Было: {old_ip}\\n\"\n                message += f\"  • Стало: {ip}\\n\"\n                message += f\"  • MAC: {mac}\\n\"\n                message += f\"  • Имя: {dev.get('name', 'Unknown')}\\n\\n\"\n        return message\n\n    def load_message_queue(self):\n        \"\"\"Загружает очередь сообщений из файла.\n        Каждая запись – словарь с ключами: \"timestamp\", \"message\" и \"type\".\n        \"\"\"\n        if not os.path.exists(self.message_queue_file):\n            return []\n        try:\n            with open(self.message_queue_file, \"r\") as f:\n                return json.load(f)\n        except Exception as e:\n            logger.error(f\"Ошибка загрузки очереди сообщений: {e}\")\n            return []\n\n    def save_message_queue(self, queue):\n        \"\"\"Сохраняет очередь сообщений в JSON-файл.\"\"\"\n        try:\n            with open(self.message_queue_file, \"w\") as f:\n                json.dump(queue, f, indent=4)\n        except Exception as e:\n            logger.error(f\"Ошибка сохранения очереди сообщений: {e}\")\n\n    def generate_inline_buttons_from_raw_message(self, raw_message):\n        \"\"\"\n        Парсит MAC-адреса из сырого сообщения, загружает кэш из файла и сравнивает с ним.\n        Для каждого MAC, присутствующего в кэше и у которого поле name_custom пустое,\n        формирует кнопку для добавления/изменения имени.\n\n        Возвращает объект InlineKeyboardMarkup или None, если кнопок нет.\n        \"\"\"\n        try:\n            # Используем регулярное выражение для поиска MAC-адресов (форматы XX:XX:XX:XX:XX:XX или XX-XX-XX-XX-XX-XX).\n            mac_pattern = r'(?:[0-9A-Fa-f]{2}[:\\-]){5}[0-9A-Fa-f]{2}'\n            mac_addresses = re.findall(mac_pattern, raw_message)\n            if not mac_addresses:\n                return None\n            network_cache = self.load_cache()\n            add_buttons = []\n            for mac in mac_addresses:\n                mac_upper = mac.upper()\n                if mac_upper in network_cache:\n                    dev = network_cache[mac_upper]\n                    # Если поле name_custom пустое, добавляем кнопку\n                    if not dev.get(\"name_custom\", \"\").strip():\n                        add_buttons.append(InlineKeyboardButton(f\"📝 {mac_upper}\", callback_data=f\"nmap_setname:{mac_upper}\"))\n            if add_buttons:\n                # Группируем кнопки по 2 в ряд (можно настроить количество столбцов)\n                rows = [add_buttons[i:i+2] for i in range(0, len(add_buttons), 2)]\n                return InlineKeyboardMarkup(rows)\n            else:\n                return None\n        except Exception as e:\n            logger.error(f\"Ошибка формирования inline-кнопок по сырому сообщению: {e}\")\n            return None\n\n\n    def _send_with_retry(self, chat_id, text, **kwargs):\n        \"\"\"\n        Отправляет сообщение с ретраями (NOTIF_MAX_RETRIES) и экспоненциальной\n        задержкой между попытками (NOTIF_RETRY_BACKOFF). Возвращает True при успехе.\n        \"\"\"\n        delay = config.NOTIF_RETRY_BACKOFF\n        for attempt in range(1, config.NOTIF_MAX_RETRIES + 1):\n            try:\n                self.bot.send_message(chat_id=chat_id, text=text, **kwargs)\n                return True\n            except Exception as e:\n                logger.warning(\n                    f\"Отправка сообщения для {chat_id}: попытка {attempt}/{config.NOTIF_MAX_RETRIES} не удалась: {e}\"\n                )\n                if attempt < config.NOTIF_MAX_RETRIES:\n                    time.sleep(delay)\n                    delay *= 2\n        return False\n\n    def send_notification(self, raw_message, notif_type, disable_notif=False):\n        \"\"\"\n        Отправляет уведомление определённого типа (notif_type) администраторам.\n        Все уведомления одного типа отправляются отдельно – никакого комбинирования разных типов.\n        Для каждого администратора проверяется наличие права:\n          - для типа \"wireguard\" – receive_notifications_wireguard,\n          - для \"guacamole\" – receive_notifications_guacamole,\n          - для \"f2b\" – receive_notifications_f2b,\n          - для \"status\" – receive_notifications_status.\n        Параметр disable_notif (или настройка receive_notifications_no_sound) влияет на флаг disable_notification.\n        \"\"\"\n        # Определяем требуемое право для данного типа уведомления\n        permission_mapping = {\n            \"wireguard\": \"receive_notifications_wireguard\",\n            \"guacamole\": \"receive_notifications_guacamole\",\n            \"f2b\": \"receive_notifications_f2b\",\n            \"status\": \"receive_notifications_status\",\n            \"nmap\": \"receive_notifications_nmap\"\n        }\n        permission_key = permission_mapping.get(notif_type)\n        if not permission_key:\n            logger.error(f\"Неизвестный тип уведомлений: {notif_type}\")\n            return\n\n        # Весь блок под локом: очереди читаются/пишутся из двух потоков (быстрые и долгие\n        # проверки), поэтому чтение->отправка->сохранение должны быть атомарными,\n        # иначе возможна потеря или дублирование уведомлений.\n        with self._queue_lock:\n            # Загружаем очередь и фильтруем записи по notif_type\n            queue = self.load_message_queue()\n            filtered_queue = [msg for msg in queue if msg.get(\"type\") == notif_type]\n\n            # Формируем список сообщений для отправки. Длинные (агрегаты nmap,\n            # очередь) режем на части по 3000 символов — иначе Telegram\n            # отвечает 'Message is too long', ретраи спамят в лог, а текст\n            # вечно перекладывается в очередь и не доходит.\n            messages = []\n            if raw_message:\n                messages.append(raw_message)\n            for queued_msg in filtered_queue:\n                messages.append(queued_msg[\"message\"] + \" [Отправлено с задержкой, первоначальное время: \" + queued_msg[\"timestamp\"] + \"]\\n\\n\")\n\n            if not messages:\n                return  # нечего отправлять\n\n            # Разбиение: [(чанк, последний_ли)] — клавиатуры вешаем только\n            # на последний чанк каждого логического сообщения.\n            outgoing = []\n            for message in messages:\n                chunks = split_tg_message(message)\n                for ci, ch in enumerate(chunks):\n                    outgoing.append((ch, ci == len(chunks) - 1))\n\n            # Новая логика для типа nmap: добавляем inline-клавиатуру с кнопками \"Добавить имя\"\n            additional_markup = self.generate_inline_buttons_from_raw_message(\"\\n\".join(messages))\n\n            # Кнопки блокировки для новых устройств (право network_manage)\n            acl_block_rows = []\n            if notif_type == \"nmap\":\n                new_macs = _extract_new_device_macs(\"\\n\".join(messages))\n                for i in range(0, len(new_macs), 2):\n                    acl_block_rows.append([\n                        InlineKeyboardButton(\n                            f\"📔 Блокировать {m}\",\n                            callback_data=f\"acl_ask:{m}\")\n                        for m in new_macs[i:i+2]\n                    ])\n\n            logger.info(f\"Отправка уведомления типа {notif_type}: \"\n                        f\"{len(messages)} сообщ., {len(outgoing)} чанков, \"\n                        f\"всего {sum(len(m) for m in messages)} символов\")\n            any_failed = False\n\n            for admin_id in self.admin_ids:\n                can_receive, no_sound = self.rights_checker.should_notify(admin_id, permission_key)\n                if not can_receive:\n                    continue\n                try:\n                    # Права — только через checker (ns-aware: ищет\n                    # 'telegram:<id>'). Прямой rights.get(str_id) мимо\n                    # ключей 'telegram:*' — так клавиатура не появлялась\n                    # вообще никогда.\n                    has = self.rights_checker.has_right\n                    # Определяем disable_notification: совмещаем переданный параметр и настройку \"без звука\"\n                    send_disable = disable_notif or no_sound\n                    if has(admin_id, \"nmap_manage\") or has(admin_id, \"all\"):\n                        reply_markup_sent = additional_markup\n                    else:\n                        reply_markup_sent = None\n                    # Кнопки блокировки новых устройств — только с правом network_manage\n                    if acl_block_rows and (has(admin_id, \"network_manage\") or has(admin_id, \"all\")):\n                        base_rows = list(reply_markup_sent.inline_keyboard) if reply_markup_sent else []\n                        reply_markup_sent = InlineKeyboardMarkup(base_rows + acl_block_rows)\n                    for chunk, is_last in outgoing:\n                        ok = self._send_with_retry(\n                            admin_id,\n                            escape_markdown_v2(chunk),\n                            disable_notification=send_disable,\n                            parse_mode='MarkdownV2',\n                            reply_markup=reply_markup_sent if is_last else None\n                        )\n                        if not ok:\n                            any_failed = True\n                except Exception as e:\n                    logger.warning(f\"Ошибка отправки уведомления для {admin_id}: {e}\")\n                    any_failed = True\n\n            if any_failed and raw_message:\n                # Если не удалось доставить – кладём сообщение в очередь, избегая дублей.\n                unsent = self.load_message_queue()\n                if not any(m.get(\"type\") == notif_type and m.get(\"message\") == raw_message for m in unsent):\n                    timestamp = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n                    unsent.append({\"timestamp\": timestamp, \"message\": raw_message, \"type\": notif_type})\n                    self.save_message_queue(unsent)\n            elif not any_failed:\n                # Все доставлено успешно – очищаем из очереди записи, относящиеся к данному типу\n                new_queue = [msg for msg in queue if msg.get(\"type\") != notif_type]\n                self.save_message_queue(new_queue)\n\n    def start_long_checks(self):\n        \"\"\"\n        Долгие проверки (сканирование сети с помощью nmap)\n        выполняются с интервалом self.long_check_interval.\n\n        Важное событие (появление нового устройства) отправляется сразу.\n        Неважные события (переподключения/отключения) копятся в буфере и отправляются\n        одним сообщением после накопления AGGREGATION_MAX_CHARS символов или по\n        истечении AGGREGATION_INTERVAL секунд с первого события в буфере.\n        \"\"\"\n        pending = []\n        pending_first_ts = None\n        while True:\n            logger.debug(\"Запуск долгих проверок: сканирование сети...\")\n            scanned_devices = self.perform_network_scan()\n            diff = self.update_cache_and_diff(scanned_devices)\n            if self._on_diff is not None:\n                try:\n                    self._on_diff(diff)\n                except Exception:  # noqa: BLE001\n                    logger.exception(\"on_diff hook failed\")\n\n            significant = bool(diff.get(\"new\"))\n            raw_message = self.produce_notification(diff)\n            now = time.time()\n\n            def flush_minor():\n                nonlocal pending, pending_first_ts\n                if pending:\n                    self.send_notification(\"\\n\\n\".join(pending), notif_type=\"nmap\", disable_notif=True)\n                    pending = []\n                    pending_first_ts = None\n\n            if raw_message:\n                if significant:\n                    # Важное событие: сначала отправляем накопленные мелочи, затем само событие со звуком.\n                    flush_minor()\n                    self.send_notification(raw_message, notif_type=\"nmap\", disable_notif=False)\n                else:\n                    pending.append(raw_message)\n                    if pending_first_ts is None:\n                        pending_first_ts = now\n                    combined_len = sum(len(m) for m in pending)\n                    if (combined_len >= config.AGGREGATION_MAX_CHARS\n                            or (now - pending_first_ts) >= config.AGGREGATION_INTERVAL):\n                        flush_minor()\n            else:\n                if pending and (now - pending_first_ts) >= config.AGGREGATION_INTERVAL:\n                    flush_minor()\n                else:\n                    with self._queue_lock:\n                        nmap_queue = [msg for msg in self.load_message_queue() if msg.get(\"type\") == \"nmap\"]\n                    if nmap_queue:\n                        self.send_notification(\"\", notif_type=\"nmap\", disable_notif=True)\n            time.sleep(self.long_check_interval)\n\n    def start_scan(self):\n        \"\"\"Долгие проверки (сканирование сети). Мониторы доп. источников\n        планирует модуль network (register_monitor), а не сканер.\"\"\"\n        Thread(target=self.start_long_checks).start()\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> subprocess</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> time</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> datetime </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> datetime, timedelta</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Thread</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> telegram </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardButton, InlineKeyboardMarkup</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> .fail2ban_mon </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Fail2BanMonitor  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># BOTMOD: штатный монитор сканера</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> copy</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># BOTMOD: было import config</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">logger </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging.getLogger(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__name__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> escape_markdown_v2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(text):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Экранирует специальные символы для MarkdownV2.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Содержимое fenced-блоков кода (между ```) не экранируется,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    иначе символы внутри блоков отображались бы с лишними обратными слэшами.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    in_code </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    result </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> text.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line.strip().startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"```\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            in_code </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> in_code</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            result.append(line)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> in_code:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            result.append(line)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            result.append(re.sub(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">([_*</span><span style=\"color:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold\">\\[\\]</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">()~>#+</span><span style=\"color:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold\">\\-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">=|{}.!])</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'</span><span style=\"color:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold\">\\\\</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">\\1</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, line))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(result)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> format_duration</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(seconds):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(timedelta(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">seconds</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">seconds))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> is_host_reachable</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ip):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Проверяет, доступен ли хост через пинг.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Возвращает True, если хотя бы один пакет успешно дошёл.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Подробный пинг (10 пакетов) важен для проверки \"отключаемых\" устройств:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # мобильные устройства (телефоны) могут не всегда отвечать на nmap-скан,</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # поэтому чтобы не считать ложное отключение, даём им достаточно попыток.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        result </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> subprocess.run(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ping\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-c\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"10\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, ip],</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            stdout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">subprocess.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">PIPE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            stderr</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">subprocess.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">PIPE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">            timeout</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">20</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        )</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Успех, если код возврата 0</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> result.returncode </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка проверки пинга для </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ip</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _extract_new_device_macs</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(text):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Извлекает MAC-адреса новых устройств (секции, начинающиеся с 🚨).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    out </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re.finditer(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\U0001F6A8.</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">{0,300}?</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">MAC:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\s</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">((?:[0-9A-Fa-f]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">{2}</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">[:-])</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">{5}</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">[0-9A-Fa-f]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">{2}</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                         text, re.S):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m.group(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).upper().replace(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"-\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\":\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> out:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            out.append(mac)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> out</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Лимит Telegram — 4096 символов, но MarkdownV2-экранирование раздувает</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># текст (каждый спецсимвол + обратный слэш). Режем с запасом: 3000 сырых.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TG_CHUNK_LIMIT</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 3000</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> split_tg_message</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(text, limit</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TG_CHUNK_LIMIT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Режет длинное уведомление на части по границам блоков (пустая строка).</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Блоки устройств (~150 символов) никогда не рвутся пополам; одиночный</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    сверхдлинный блок без '</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">' режется жёстко по лимиту.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    text </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> text </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(text) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> limit:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [text]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    parts, cur, cur_len </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [], [], </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> block </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> text.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        b </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> block.strip(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> b:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(b) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> limit:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # аварийный случай: сам блок больше лимита — режем жёстко</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cur:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                parts.append(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(cur))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                cur, cur_len </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [], </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> i </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> range</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(b), limit):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                parts.append(b[i:i </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> limit])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            continue</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cur </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cur_len </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(b) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 2</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> limit:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            parts.append(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(cur))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            cur, cur_len </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [], </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        cur.append(b)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        cur_len </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(b) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 2</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cur:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        parts.append(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(cur))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> parts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [text]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">class</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> NetworkScanner</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> __init__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, message_queue, cache_file, wg_cache_file, monitoring_script, ping_script,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                 ag_script, ag_cache, f2b_enabled, f2b_script, f2b_cache, admin_ids, rights_checker_class, sender,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                 network_range, long_check_interval, fast_check_interval, mac_filter_file,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                 on_diff</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.message_queue_file </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> message_queue  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># путь к файлу очереди сообщений</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._queue_lock </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> threading.Lock()  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># защищает очередь сообщений от гонок потоков</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.cache_file </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache_file</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.admin_ids </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> admin_ids</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.rights_checker </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rights_checker_class</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # sender — шлюз messengers.sender.GatewaySender (активный транспорт).</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.bot </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sender</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.network_range </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> network_range</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.long_check_interval </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> long_check_interval  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># интервал для долгих проверок (сканирование сети)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.fast_check_interval </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> fast_check_interval  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># интервал для быстрых проверок (мониторинг доп. источников)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.mac_filter_file </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac_filter_file</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.excluded_macs </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.load_mac_filter()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.wg_monitor </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">  # BOTMOD: мониторы доп. источников вешает модуль (register_monitor)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ag_monitor </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.f2b_monitor </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Fail2BanMonitor(f2b_cache, f2b_script)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # on_diff(diff) — хук сырых diff скана для шины событий (ставит модуль).</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._on_diff </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> on_diff</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> load_cache</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Загружает кэш из JSON-файла\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.exists(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.cache_file):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.cache_file, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'r'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json.load(f)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка загрузки кэша: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> save_cache</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, cache):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Сохраняет кэш в JSON-файл\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.cache_file, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'w'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            json.dump(cache, f, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">indent</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">4</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> load_mac_filter</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Загружает список MAC-адресов, которые нужно исключить, из JSON файла.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.exists(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.mac_filter_file):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.mac_filter_file, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'r'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                mac_list </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json.load(f)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(mac_list, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">list</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                logger.error(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Формат JSON для фильтра MAC должен быть списком.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Приводим все MAC-адреса к верхнему регистру для корректного сравнения</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [mac.upper() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac_list]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка загрузки MAC фильтра: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> perform_network_scan</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Выполняет sweep-скан сети с помощью nmap:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        nmap -sn -T4 --privileged &#x3C;NETWORK_RANGE></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Возвращает словарь устройств.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        cmd </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'nmap'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'-sn'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'-T4'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'--privileged'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.network_range]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            result </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> subprocess.run(cmd, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">capture_output</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">check</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> subprocess.CalledProcessError </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка сканирования сети: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        devices </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        current_device </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.excluded_macs </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.load_mac_filter()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> result.stdout.splitlines():</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            line </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line.strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Nmap scan report for\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> '('</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> ')'</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    ip </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'('</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">')'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    hostname </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'('</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].split()[</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    ip </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line.split()[</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    hostname </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Unknown\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                current_device </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: ip,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: hostname,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"mac\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"ports\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: [],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Unknown\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MAC Address:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> current_device </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                parts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line.split()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> parts[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].upper()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.excluded_macs:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    current_device </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                current_device[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mac\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"(\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \")\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    vendor </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"(\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\")\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    current_device[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> vendor.strip()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                devices[current_device[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mac\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> current_device</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                current_device </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> devices</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> perform_port_scan</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, ip):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Выполняет полный портскан для указанного IP:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        nmap -p- --open -T4 --privileged &#x3C;ip></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Возвращает список открытых портов.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        cmd </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'nmap'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'-p-'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'--open'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'-T5'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, ip]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            result </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> subprocess.run(cmd, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">capture_output</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">check</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> subprocess.CalledProcessError </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка сканирования портов для </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ip</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ports </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> result.stdout.splitlines():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"/tcp\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"open\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                port </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                ports.append(port)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ports </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ports </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Нет открытых портов\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> add_ignored_mac</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, mac):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac.upper()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.load_cache()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">             return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MAC адрес не найден в кэше сети.\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ignored </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.load_mac_filter()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ignored:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">             return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MAC адрес уже присутствует в игнор-листе.\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ignored.append(mac)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">             with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.mac_filter_file, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"w\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                  json.dump(ignored, f, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">indent</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">4</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">             return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MAC адрес успешно добавлен в игнор-лист.\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">             return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка сохранения: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> remove_ignored_mac</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, mac):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac.upper()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ignored </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.load_mac_filter()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ignored:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">             return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MAC адрес отсутствует в игнор-листе.\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ignored.remove(mac)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">             with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.mac_filter_file, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"w\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                  json.dump(ignored, f, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">indent</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">4</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">             return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MAC адрес успешно удалён из игнор-листа.\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">             return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка сохранения: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _ensure_stats</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, dev, now):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Гарантирует наличие полей статистики у устройства и выполняет сброс</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        суточных счётчиков при смене календарного дня.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"connections_total\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connections_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"connections_today\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connections_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"online_seconds_total\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_seconds_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"online_seconds_today\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_seconds_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"online_start_ts\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_start_ts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        today </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> datetime.now().strftime(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"%Y-%m-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"stats_date\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> today:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"stats_date\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> today</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connections_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_seconds_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_start_ts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_start_ts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> now</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> update_cache_and_diff</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, scanned_devices):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Сравнивает данные текущего сканирования с кэшем, обновляет статус `connected`</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        и возвращает diff, при этом пытаясь сохранить изменения (например, индивидуальное</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        кастомное имя) внесённые параллельно другим процессом.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Шаг 1. Загружаем исходный кэш и делаем глубокую копию для редактирования.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        baseline_cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.load_cache()         </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># эталонная версия</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        editable_cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> copy.deepcopy(baseline_cache)  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># версия для редактирования</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> time.time()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        diff </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"new\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {}, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"reconnected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {}, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"disconnected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {}, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip_changed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: {}}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Шаг 2. Обработка данных сканирования (scanned_devices)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac, dev </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> scanned_devices.items():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> editable_cache:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"first_appearance\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> now</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"last_appearance\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> now</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"last_disconnection\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ports\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.perform_port_scan(dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"miss_count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"stats_date\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> datetime.now().strftime(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"%Y-%m-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connections_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connections_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_seconds_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_seconds_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_start_ts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> now</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                editable_cache[mac] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                diff[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"new\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">][mac] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                cached </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> editable_cache[mac]</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                # Сохраняем ранее установленные значения, в том числе кастомное имя.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"first_appearance\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cached.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"first_appearance\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, now)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"last_disconnection\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cached.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"last_disconnection\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cached.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                # Устройство видно в сканировании — сбрасываем счётчик пропусков.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"miss_count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                # Переносим накопленную статистику из кэша и делаем суточный сброс при необходимости.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connections_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cached.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connections_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connections_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cached.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connections_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_seconds_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cached.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_seconds_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_seconds_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cached.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_seconds_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_start_ts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cached.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_start_ts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"stats_date\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cached.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"stats_date\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._ensure_stats(dev, now)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                # Определяем смену IP-адреса (например, при обновлении DHCP-аренды).</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                old_ip </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cached.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> old_ip </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> old_ip </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    ip_info </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> copy.deepcopy(dev)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    ip_info[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"old_ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> old_ip</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    diff[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip_changed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">][mac] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ip_info</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cached.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"last_appearance\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> now</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ports\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.perform_port_scan(dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connections_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connections_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_start_ts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> now</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    editable_cache[mac] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    diff[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"reconnected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">][mac] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"last_appearance\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cached.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"last_appearance\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, now)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ports\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cached.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ports\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, [])</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    editable_cache[mac] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Обработка устройств, которых нет в текущем сканировании.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Отключённым считаем только после DEBOUNCE_COUNT пропусков подряд</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # (нет в nmap И не отвечает на пинг), чтобы не срабатывать на ложные пропадания.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> list</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(editable_cache.keys()):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> scanned_devices </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> editable_cache[mac].get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                dev </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> editable_cache[mac]</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._ensure_stats(dev, now)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                ip </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                reachable </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ip) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> is_host_reachable(ip)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> reachable:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"miss_count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    miss </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"miss_count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"miss_count\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> miss</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> miss </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">>=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">DEBOUNCE_COUNT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                        # Начисляем время онлайн и закрываем интервал.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        online_start </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_start_ts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> online_start:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                            dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_seconds_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_seconds_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> online_start)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                            today_start </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> datetime.now().replace(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">hour</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">minute</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">second</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">microsecond</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).timestamp()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                            dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_seconds_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_seconds_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> max</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(online_start, today_start))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                            dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_start_ts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"last_disconnection\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> now</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        dev[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        diff[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"disconnected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">][mac] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Шаг 3. Прежде чем сохранить, пытаемся сохранить изменения, внесённые параллельно.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        current_cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.load_cache()  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># Текущая версия кэша из файла</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> current_cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> baseline_cache:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Для каждого MAC-адреса проверяем, если поле name_custom изменилось в current_cache,</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # то обновляем его в editable_cache.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac, current_dev </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> current_cache.items():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> baseline_cache:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                    # Если значение name_custom в текущем кэше отличается от эталонного,</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                    # то используем его в editable_cache.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    baseline_name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> baseline_cache[mac].get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    current_name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> current_dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> current_name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> baseline_name:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                        # Обновляем кастомное имя в редактируемом кэше</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> editable_cache:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                            editable_cache[mac][</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> current_name</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                    # Если новое устройство появилось в current_cache, добавляем его в editable_cache.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._ensure_stats(current_dev, now)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    editable_cache[mac] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> current_dev</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Шаг 4. Сохраняем полностью обновлённый кэш и возвращаем сформированный diff.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.save_cache(editable_cache)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> diff</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> produce_notification</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, diff):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Формирует сообщение об изменениях в сети.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Новые устройства</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> diff.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"new\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac, dev </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> diff[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"new\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].items():</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                rule </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"new\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                ip </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Unknown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚨 </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ip</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">rule</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">):</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"```</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MAC: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mac</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Name: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'name'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'Unknown'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ports: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">', '</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'ports'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, []))</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Type: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'type'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'Unknown'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"First appearance: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">datetime.fromtimestamp(dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'first_appearance'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)).strftime(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'%Y-%m-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> %H:%M:%S'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"```</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Повторное подключение</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> diff.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"reconnected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac, dev </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> diff[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"reconnected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].items():</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                last_disconnection </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"last_disconnection\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                name_custom </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                ip </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Unknown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> last_disconnection </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    duration </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> format_duration(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(time.time() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> last_disconnection))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    rule </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connected after </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">duration</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> offline\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    rule </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"connected\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name_custom:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔄 </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name_custom</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">rule</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">):</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔄 </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ip</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">rule</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">):</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"```</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name_custom:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"IP: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ip</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MAC: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mac</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Name: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'name'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'Unknown'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ports: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">', '</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'ports'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, []))</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Type: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'type'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'Unknown'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"First appearance: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">datetime.fromtimestamp(dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'first_appearance'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)).strftime(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'%Y-%m-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> %H:%M:%S'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"```</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Отключившиеся устройства</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> diff.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"disconnected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac, dev </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> diff[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"disconnected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].items():</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                last_appearance </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"last_appearance\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                name_custom </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                ip </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Unknown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> last_appearance </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    duration </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> format_duration(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(time.time() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> last_appearance))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    rule </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"disconnected after </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">duration</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> online\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    rule </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"disconnected\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name_custom:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔌 </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name_custom</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">rule</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">):</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔌 </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ip</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">rule</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">):</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"```</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name_custom:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"IP: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ip</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MAC: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mac</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Name: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'name'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'Unknown'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ports: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">', '</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'ports'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, []))</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Type: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'type'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'Unknown'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"First appearance: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">datetime.fromtimestamp(dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'first_appearance'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)).strftime(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'%Y-%m-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> %H:%M:%S'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"```</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Смена IP-адреса</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> diff.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip_changed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac, dev </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> diff[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip_changed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">].items():</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                name_custom </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                ip </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Unknown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                old_ip </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"old_ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Unknown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                title </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name_custom </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name_custom </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ip</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📍 </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">title</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> сменил IP:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  • Было: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">old_ip</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  • Стало: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ip</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  • MAC: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mac</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  • Имя: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'name'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'Unknown'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}\\n\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> message</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> load_message_queue</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Загружает очередь сообщений из файла.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Каждая запись – словарь с ключами: \"timestamp\", \"message\" и \"type\".</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os.path.exists(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.message_queue_file):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.message_queue_file, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"r\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json.load(f)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка загрузки очереди сообщений: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> save_message_queue</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, queue):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Сохраняет очередь сообщений в JSON-файл.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.message_queue_file, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"w\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                json.dump(queue, f, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">indent</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">4</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка сохранения очереди сообщений: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> generate_inline_buttons_from_raw_message</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, raw_message):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Парсит MAC-адреса из сырого сообщения, загружает кэш из файла и сравнивает с ним.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Для каждого MAC, присутствующего в кэше и у которого поле name_custom пустое,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        формирует кнопку для добавления/изменения имени.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Возвращает объект InlineKeyboardMarkup или None, если кнопок нет.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Используем регулярное выражение для поиска MAC-адресов (форматы XX:XX:XX:XX:XX:XX или XX-XX-XX-XX-XX-XX).</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            mac_pattern </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">(?:[0-9A-Fa-f]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">{2}</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">[:</span><span style=\"color:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold\">\\-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">])</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">{5}</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">[0-9A-Fa-f]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">{2}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            mac_addresses </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re.findall(mac_pattern, raw_message)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac_addresses:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            network_cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.load_cache()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            add_buttons </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac_addresses:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                mac_upper </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac.upper()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac_upper </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> network_cache:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    dev </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> network_cache[mac_upper]</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                    # Если поле name_custom пустое, добавляем кнопку</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> dev.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip():</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        add_buttons.append(InlineKeyboardButton(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📝 </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mac_upper</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap_setname:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mac_upper</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> add_buttons:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                # Группируем кнопки по 2 в ряд (можно настроить количество столбцов)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                rows </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [add_buttons[i:i</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> i </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> range</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(add_buttons), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardMarkup(rows)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка формирования inline-кнопок по сырому сообщению: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _send_with_retry</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, chat_id, text, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">**</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">kwargs):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Отправляет сообщение с ретраями (NOTIF_MAX_RETRIES) и экспоненциальной</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        задержкой между попытками (NOTIF_RETRY_BACKOFF). Возвращает True при успехе.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        delay </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">NOTIF_RETRY_BACKOFF</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> attempt </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> range</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">NOTIF_MAX_RETRIES</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.bot.send_message(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">chat_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">chat_id, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">text, </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">**</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">kwargs)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                logger.warning(</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Отправка сообщения для </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">chat_id</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: попытка </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">attempt</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">NOTIF_MAX_RETRIES}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> не удалась: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                )</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> attempt </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">NOTIF_MAX_RETRIES</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    time.sleep(delay)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    delay </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 2</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> send_notification</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, raw_message, notif_type, disable_notif</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Отправляет уведомление определённого типа (notif_type) администраторам.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Все уведомления одного типа отправляются отдельно – никакого комбинирования разных типов.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Для каждого администратора проверяется наличие права:</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          - для типа \"wireguard\" – receive_notifications_wireguard,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          - для \"guacamole\" – receive_notifications_guacamole,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          - для \"f2b\" – receive_notifications_f2b,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">          - для \"status\" – receive_notifications_status.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Параметр disable_notif (или настройка receive_notifications_no_sound) влияет на флаг disable_notification.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Определяем требуемое право для данного типа уведомления</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        permission_mapping </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"wireguard\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_wireguard\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"guacamole\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_guacamole\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"f2b\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_f2b\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"status\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_status\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"nmap\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"receive_notifications_nmap\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        permission_key </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> permission_mapping.get(notif_type)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> permission_key:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Неизвестный тип уведомлений: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">notif_type</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Весь блок под локом: очереди читаются/пишутся из двух потоков (быстрые и долгие</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # проверки), поэтому чтение->отправка->сохранение должны быть атомарными,</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # иначе возможна потеря или дублирование уведомлений.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._queue_lock:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Загружаем очередь и фильтруем записи по notif_type</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            queue </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.load_message_queue()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            filtered_queue </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> queue </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> notif_type]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Формируем список сообщений для отправки. Длинные (агрегаты nmap,</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # очередь) режем на части по 3000 символов — иначе Telegram</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # отвечает 'Message is too long', ретраи спамят в лог, а текст</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # вечно перекладывается в очередь и не доходит.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            messages </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> raw_message:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                messages.append(raw_message)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> queued_msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> filtered_queue:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                messages.append(queued_msg[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"message\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" [Отправлено с задержкой, первоначальное время: \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> queued_msg[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"timestamp\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"]</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> messages:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span><span style=\"color:#6A737D;--shiki-dark:#6A737D\">  # нечего отправлять</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Разбиение: [(чанк, последний_ли)] — клавиатуры вешаем только</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # на последний чанк каждого логического сообщения.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            outgoing </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> messages:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                chunks </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> split_tg_message(message)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ci, ch </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> enumerate</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(chunks):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    outgoing.append((ch, ci </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(chunks) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Новая логика для типа nmap: добавляем inline-клавиатуру с кнопками \"Добавить имя\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            additional_markup </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.generate_inline_buttons_from_raw_message(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(messages))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Кнопки блокировки для новых устройств (право network_manage)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            acl_block_rows </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> notif_type </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"nmap\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                new_macs </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _extract_new_device_macs(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(messages))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> i </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> range</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(new_macs), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    acl_block_rows.append([</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        InlineKeyboardButton(</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                            f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📔 Блокировать </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                            callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"acl_ask:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> new_macs[i:i</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    ])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.info(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Отправка уведомления типа </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">notif_type</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: \"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(messages)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> сообщ., </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(outgoing)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> чанков, \"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"всего </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{sum</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(m) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> messages)</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> символов\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            any_failed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> admin_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.admin_ids:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                can_receive, no_sound </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.rights_checker.should_notify(admin_id, permission_key)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> can_receive:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    continue</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                    # Права — только через checker (ns-aware: ищет</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                    # 'telegram:&#x3C;id>'). Прямой rights.get(str_id) мимо</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                    # ключей 'telegram:*' — так клавиатура не появлялась</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                    # вообще никогда.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    has </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.rights_checker.has_right</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                    # Определяем disable_notification: совмещаем переданный параметр и настройку \"без звука\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    send_disable </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> disable_notif </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> no_sound</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> has(admin_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> has(admin_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"all\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        reply_markup_sent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> additional_markup</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        reply_markup_sent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                    # Кнопки блокировки новых устройств — только с правом network_manage</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> acl_block_rows </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (has(admin_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> has(admin_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"all\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        base_rows </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> list</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(reply_markup_sent.inline_keyboard) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> reply_markup_sent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        reply_markup_sent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardMarkup(base_rows </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> acl_block_rows)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> chunk, is_last </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> outgoing:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        ok </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send_with_retry(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                            admin_id,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                            escape_markdown_v2(chunk),</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                            disable_notification</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">send_disable,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                            parse_mode</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'MarkdownV2'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                            reply_markup</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">reply_markup_sent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> is_last </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        )</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ok:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                            any_failed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    logger.warning(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка отправки уведомления для </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">admin_id</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">e</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    any_failed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> any_failed </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> raw_message:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                # Если не удалось доставить – кладём сообщение в очередь, избегая дублей.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                unsent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.load_message_queue()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> any</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> notif_type </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"message\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> raw_message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> unsent):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    timestamp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> datetime.now().strftime(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"%Y-%m-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> %H:%M:%S\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    unsent.append({</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"timestamp\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: timestamp, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"message\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: raw_message, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: notif_type})</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.save_message_queue(unsent)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            elif</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> any_failed:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                # Все доставлено успешно – очищаем из очереди записи, относящиеся к данному типу</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                new_queue </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> queue </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> notif_type]</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.save_message_queue(new_queue)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> start_long_checks</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Долгие проверки (сканирование сети с помощью nmap)</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        выполняются с интервалом self.long_check_interval.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Важное событие (появление нового устройства) отправляется сразу.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        Неважные события (переподключения/отключения) копятся в буфере и отправляются</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        одним сообщением после накопления AGGREGATION_MAX_CHARS символов или по</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        истечении AGGREGATION_INTERVAL секунд с первого события в буфере.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        pending </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        pending_first_ts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        while</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.debug(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Запуск долгих проверок: сканирование сети...\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            scanned_devices </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.perform_network_scan()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            diff </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.update_cache_and_diff(scanned_devices)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._on_diff </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._on_diff(diff)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    logger.exception(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"on_diff hook failed\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            significant </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(diff.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"new\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            raw_message </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.produce_notification(diff)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> time.time()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> flush_minor</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                nonlocal</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pending, pending_first_ts</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pending:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_notification(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(pending), </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">notif_type</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">disable_notif</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    pending </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    pending_first_ts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> raw_message:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> significant:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">                    # Важное событие: сначала отправляем накопленные мелочи, затем само событие со звуком.</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    flush_minor()</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_notification(raw_message, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">notif_type</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">disable_notif</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    pending.append(raw_message)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pending_first_ts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        pending_first_ts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> now</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    combined_len </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sum</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(m) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pending)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (combined_len </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">>=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">AGGREGATION_MAX_CHARS</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                            or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pending_first_ts) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">>=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">AGGREGATION_INTERVAL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        flush_minor()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pending </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (now </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pending_first_ts) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">>=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">AGGREGATION_INTERVAL</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                    flush_minor()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._queue_lock:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                        nmap_queue </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.load_message_queue() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> msg.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"nmap\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> nmap_queue:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.send_notification(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">notif_type</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">disable_notif</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            time.sleep(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.long_check_interval)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> start_scan</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"\"\"Долгие проверки (сканирование сети). Мониторы доп. источников</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        планирует модуль network (register_monitor), а не сканер.\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        Thread(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">target</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.start_long_checks).start()</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "store.py",
          "size": 6198,
          "content": "\"\"\"network.store — единственное место IO кеша сканера и игнор-листа.\n\nВладелец файлов networkscan_cache.json / networkscan_ignored.json — пакет\nnetwork: сканер пишет, ui_web и admin-движок (nmap-страницы TG) читают/правят\nтолько через эти функции, а не прямым open().\n\"\"\"\nimport json\nimport os\n\n\ndef _paths():\n    from . import config as pkg_config\n    return pkg_config.CACHE_FILE_NETWORK, pkg_config.SCAN_NETWORK_IGNORED_MACS\n\n\ndef _read_json(path):\n    try:\n        with open(path, \"r\", encoding=\"utf-8\") as f:\n            return json.load(f)\n    except Exception:\n        return None\n\n\ndef _write_json(path, data):\n    tmp = path + \".tmp\"\n    os.makedirs(os.path.dirname(tmp) or \".\", exist_ok=True)\n    with open(tmp, \"w\", encoding=\"utf-8\") as f:\n        json.dump(data, f, indent=4, ensure_ascii=False)\n    os.replace(tmp, path)\n\n\ndef load_devices():\n    cache_file, _ = _paths()\n    data = _read_json(cache_file)\n    return data if isinstance(data, dict) else {}\n\n\ndef save_devices(cache) -> None:\n    cache_file, _ = _paths()\n    _write_json(cache_file, cache or {})\n\n\ndef set_device_name(mac, name):\n    \"\"\"Кастомное имя устройства. Вернуть (ok, err).\"\"\"\n    cache = load_devices()\n    if mac not in cache:\n        return False, \"MAC не найден в кэше\"\n    cache[mac][\"name_custom\"] = name\n    try:\n        save_devices(cache)\n    except Exception as e:  # noqa: BLE001\n        return False, str(e)\n    return True, \"\"\n\n\ndef delete_device(mac):\n    \"\"\"Удалить запись устройства из кеша. Вернуть (ok, err).\n\n    Устройство вернётся при следующем скане, если оно реально в сети, —\n    это способ сбросить историю/стат, а не вечный бан (для бана —\n    игнор-лист).\"\"\"\n    cache = load_devices()\n    if mac not in cache:\n        return False, \"MAC не найден в кэше\"\n    try:\n        del cache[mac]\n        save_devices(cache)\n    except Exception as e:  # noqa: BLE001\n        return False, str(e)\n    return True, \"\"\n\n\ndef load_ignored():\n    _, ignored_file = _paths()\n    data = _read_json(ignored_file)\n    return data if isinstance(data, list) else []\n\n\ndef devices_snapshot():\n    \"\"\"Обогащённый снимок устройств для панели мониторинга.\n\n    Порт агрегата из transport_web.monitoring (переехал к владельцу данных):\n    durations, сортировка онлайн-first. Best-effort — пустой кеш = пусто.\n    \"\"\"\n    import time as _time\n\n    def _fmt_ts(ts):\n        if not ts:\n            return None\n        return _time.strftime(\"%Y-%m-%d %H:%M:%S\", _time.localtime(ts))\n\n    def _fmt_duration(sec):\n        if sec is None or sec < 0:\n            return \"—\"\n        sec = int(sec)\n        if sec < 60:\n            return f\"{sec} сек\"\n        m, s = divmod(sec, 60)\n        h, m = divmod(m, 60)\n        d, h = divmod(h, 24)\n        parts = []\n        if d:\n            parts.append(f\"{d} д\")\n        if h:\n            parts.append(f\"{h} ч\")\n        if m:\n            parts.append(f\"{m} мин\")\n        if not parts and s:\n            parts.append(f\"{s} сек\")\n        return \" \".join(parts) or \"0 мин\"\n\n    scan = load_devices()\n    now_ts = _time.time()\n    devices = []\n    for mac, d in scan.items():\n        connected = bool(d.get(\"connected\"))\n        online_start = d.get(\"online_start_ts\")\n        last_appearance = d.get(\"last_appearance\")\n        last_disconnection = d.get(\"last_disconnection\")\n        first_appearance = d.get(\"first_appearance\")\n        if connected:\n            last_change_ts = online_start if online_start else (last_appearance or first_appearance)\n        else:\n            last_change_ts = last_disconnection if last_disconnection else (last_appearance or first_appearance)\n        duration_s = int(now_ts - last_change_ts) if last_change_ts else 0\n        if duration_s < 0:  # защита от будущих меток в кеше\n            duration_s = 0\n        devices.append({\n            \"name\": d.get(\"name_custom\") or d.get(\"name\") or mac,\n            \"ip\": d.get(\"ip\"),\n            \"mac\": mac,\n            \"online\": connected,\n            \"ports\": d.get(\"ports\") or [],\n            \"last_seen\": _fmt_ts(last_appearance),\n            \"offline_since\": _fmt_ts(last_disconnection),\n            \"first_appearance\": _fmt_ts(first_appearance),\n            \"last_change\": _fmt_ts(last_change_ts),\n            \"last_change_ts\": last_change_ts,\n            \"duration_s\": duration_s,\n            \"duration_str\": _fmt_duration(duration_s) if last_change_ts else \"—\",\n            \"connections_total\": d.get(\"connections_total\"),\n            \"connections_today\": d.get(\"connections_today\"),\n            \"online_seconds_total\": d.get(\"online_seconds_total\"),\n            \"online_seconds_today\": d.get(\"online_seconds_today\"),\n        })\n    devices.sort(key=lambda x: (not x[\"online\"], (x[\"name\"] or \"\").lower()))\n    return {\"total\": len(devices),\n            \"online\": sum(1 for d in devices if d[\"online\"]),\n            \"devices\": devices}\n\n\ndef save_ignored(ignored) -> None:\n    _, ignored_file = _paths()\n    _write_json(ignored_file, ignored or [])\n\n\ndef set_ignored(action, mac):\n    \"\"\"add/remove MAC в игнор-листе. Вернуть (ignored|None, err).\"\"\"\n    ignored = load_ignored()\n    ignored_up = [x.upper() for x in ignored]\n    if action == \"add\":\n        if mac in ignored_up:\n            return None, \"MAC уже в игнор-листе\"\n        if mac not in load_devices():\n            return None, \"MAC не найден в кэше сети\"\n        ignored.append(mac)\n    else:\n        if mac not in ignored_up:\n            return None, \"MAC нет в игнор-листе\"\n        ignored = [x for x in ignored if x.upper() != mac]\n    try:\n        save_ignored(ignored)\n    except Exception as e:  # noqa: BLE001\n        return None, str(e)\n    return ignored, \"\"\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"network.store — единственное место IO кеша сканера и игнор-листа.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Владелец файлов networkscan_cache.json / networkscan_ignored.json — пакет</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">network: сканер пишет, ui_web и admin-движок (nmap-страницы TG) читают/правят</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">только через эти функции, а не прямым open().</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> os</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _paths</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> config </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pkg_config</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pkg_config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">CACHE_FILE_NETWORK</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, pkg_config.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">SCAN_NETWORK_IGNORED_MACS</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _read_json</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(path):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(path, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"r\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> json.load(f)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _write_json</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(path, data):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    tmp </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> path </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \".tmp\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    os.makedirs(os.path.dirname(tmp) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \".\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">exist_ok</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    with</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> open</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(tmp, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"w\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">encoding</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"utf-8\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> f:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        json.dump(data, f, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">indent</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">4</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">ensure_ascii</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    os.replace(tmp, path)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> load_devices</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">():</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    cache_file, _ </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _paths()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _read_json(cache_file)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> save_devices</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(cache) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    cache_file, _ </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _paths()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _write_json(cache_file, cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {})</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> set_device_name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(mac, name):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Кастомное имя устройства. Вернуть (ok, err).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> load_devices()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MAC не найден в кэше\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    cache[mac][</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> name</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        save_devices(cache)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> delete_device</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(mac):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Удалить запись устройства из кеша. Вернуть (ok, err).</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Устройство вернётся при следующем скане, если оно реально в сети, —</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    это способ сбросить историю/стат, а не вечный бан (для бана —</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    игнор-лист).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> load_devices()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MAC не найден в кэше\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        del</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache[mac]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        save_devices(cache)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> load_ignored</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">():</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _, ignored_file </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _paths()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _read_json(ignored_file)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">list</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> devices_snapshot</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">():</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Обогащённый снимок устройств для панели мониторинга.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Порт агрегата из transport_web.monitoring (переехал к владельцу данных):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    durations, сортировка онлайн-first. Best-effort — пустой кеш = пусто.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> time </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _time</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _fmt_ts</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ts):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ts:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _time.strftime(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"%Y-%m-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> %H:%M:%S\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, _time.localtime(ts))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _fmt_duration</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(sec):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sec </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">is</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sec </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"—\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        sec </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(sec)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sec </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 60</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">sec</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> сек\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        m, s </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> divmod</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(sec, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">60</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        h, m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> divmod</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(m, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">60</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        d, h </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> divmod</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(h, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">24</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        parts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            parts.append(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> д\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> h:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            parts.append(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">h</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> ч\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            parts.append(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">m</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> мин\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> parts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">and</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            parts.append(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">f</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">s</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> сек\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" \"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(parts) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"0 мин\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    scan </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> load_devices()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    now_ts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _time.time()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    devices </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac, d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> scan.items():</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        connected </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        online_start </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_start_ts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        last_appearance </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"last_appearance\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        last_disconnection </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"last_disconnection\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        first_appearance </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"first_appearance\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> connected:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            last_change_ts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> online_start </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> online_start </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (last_appearance </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> first_appearance)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            last_change_ts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> last_disconnection </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> last_disconnection </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (last_appearance </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> first_appearance)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        duration_s </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(now_ts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> last_change_ts) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> last_change_ts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> duration_s </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># защита от будущих меток в кеше</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            duration_s </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        devices.append({</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"mac\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: mac,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"online\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: connected,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"ports\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ports\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"last_seen\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _fmt_ts(last_appearance),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"offline_since\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _fmt_ts(last_disconnection),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"first_appearance\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _fmt_ts(first_appearance),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"last_change\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _fmt_ts(last_change_ts),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"last_change_ts\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: last_change_ts,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"duration_s\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: duration_s,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"duration_str\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: _fmt_duration(duration_s) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> last_change_ts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"—\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"connections_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connections_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"connections_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connections_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"online_seconds_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_seconds_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"online_seconds_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_seconds_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        })</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    devices.sort(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">key</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=lambda</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x: (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">], (x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).lower()))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(devices),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"online\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">sum</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> devices </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"devices\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: devices}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> save_ignored</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ignored) -> </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _, ignored_file </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _paths()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    _write_json(ignored_file, ignored </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [])</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> set_ignored</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(action, mac):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"add/remove MAC в игнор-листе. Вернуть (ignored|None, err).\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    ignored </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> load_ignored()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    ignored_up </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [x.upper() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ignored]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> action </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"add\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ignored_up:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MAC уже в игнор-листе\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> load_devices():</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MAC не найден в кэше сети\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ignored.append(mac)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ignored_up:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"MAC нет в игнор-листе\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ignored </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [x </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ignored </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x.upper() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        save_ignored(ignored)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ignored, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "tg.py",
          "size": 16663,
          "content": "\"\"\"network.tg — TG-меню сканирования (паритет с продом).\n\nСтатус/статистика с пагинацией, кастомные имена MAC, игнор-лист.\nДанные — devices_snapshot + store пакета (имена/игнор).\nПросмотр — view_net_status, изменения — nmap_manage.\nLegacy-регистрация в ui_tg (группа -1; ввод текста — группа -6).\n\"\"\"\nimport logging\nimport re\n\nfrom telegram import InlineKeyboardButton, InlineKeyboardMarkup\nfrom telegram.ext import DispatcherHandlerStop\n\nfrom . import store as _store\n\nlogger = logging.getLogger(__name__)\n\n_VIEW_RIGHT = \"view_net_status\"\n_EDIT_RIGHT = \"nmap_manage\"\n_PAGE = 8\n_MAC_RE = r\"^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$\"\n\n\ndef _qid(uid):\n    s = str(uid)\n    return s if \":\" in s else \"telegram:%s\" % s\n\n\ndef _display(d):\n    return (d.get(\"name_custom\") or d.get(\"name\") or d.get(\"mac\")\n            or \"Unknown\")\n\n\nclass NetMenu:\n    def __init__(self, ctx):\n        self.ctx = ctx\n\n    def _can_view(self, user_id):\n        try:\n            return bool(self.ctx.rights.can(_qid(user_id), _VIEW_RIGHT))\n        except Exception:  # noqa: BLE001\n            return False\n\n    def _can_edit(self, user_id):\n        try:\n            return bool(self.ctx.rights.can(_qid(user_id), _EDIT_RIGHT))\n        except Exception:  # noqa: BLE001\n            return False\n\n    def _devices(self):\n        try:\n            snap = self.ctx.api.call(\"network.devices_snapshot\") or {}\n        except Exception as e:  # noqa: BLE001\n            logger.error(\"network TG snapshot: %s\", e)\n            return []\n        devs = snap.get(\"devices\") or []\n        return [d for d in devs if isinstance(d, dict)]\n\n    def _send(self, bot, chat_id, text, reply_markup=None):\n        try:\n            bot.send_message(chat_id=chat_id, text=text,\n                             reply_markup=reply_markup)\n        except Exception as e:  # noqa: BLE001\n            logger.error(\"network TG send: %s\", e)\n\n    # -- меню ---------------------------------------------------------\n    def show_menu(self, update, context):\n        query = update.callback_query\n        try:\n            query.answer()\n        except Exception:  # noqa: BLE001\n            pass\n        user_id = update.effective_user.id\n        chat_id = update.effective_chat.id\n        if not self._can_view(user_id):\n            self._send(context.bot, chat_id, \"🚫 Доступ запрещён!\")\n            return\n        kb = InlineKeyboardMarkup([\n            [InlineKeyboardButton(\"📊 Статус сети\",\n                                  callback_data=\"net_status\")],\n            [InlineKeyboardButton(\"📈 Статистика устройств\",\n                                  callback_data=\"net_stats\")],\n            [InlineKeyboardButton(\"📝 Изменить имя для MAC\",\n                                  callback_data=\"net_addname\")],\n            [InlineKeyboardButton(\"🔇 Фильтр MAC\",\n                                  callback_data=\"net_ignore\")],\n            [InlineKeyboardButton(\"🏠 Главное меню\",\n                                  callback_data=\"main_menu\")],\n        ])\n        self._send(context.bot, chat_id, \"Меню сканирования:\", kb)\n\n    # -- статус --------------------------------------------------------\n    def show_status(self, update, context, page=1):\n        query = update.callback_query\n        try:\n            query.answer()\n        except Exception:  # noqa: BLE001\n            pass\n        user_id = update.effective_user.id\n        chat_id = update.effective_chat.id\n        if not self._can_view(user_id):\n            self._send(context.bot, chat_id, \"🚫 Доступ запрещён!\")\n            return\n        devs = self._devices()\n        if not devs:\n            self._send(context.bot, chat_id, \"Ошибка загрузки кэша сети.\")\n            return\n        total_pages = max(1, (len(devs) + _PAGE - 1) // _PAGE)\n        page = max(1, min(page, total_pages))\n        chunk = devs[(page - 1) * _PAGE:page * _PAGE]\n        header = \"Обзор сети\"\n        if total_pages > 1:\n            header += \" (стр. %s/%s)\" % (page, total_pages)\n        lines = [header, \"\"]\n        for d in chunk:\n            st = \"🔄\" if d.get(\"online\") else \"🔌\"\n            lines.append(\"%s %s (%s) - %s\" % (\n                st, d.get(\"mac\") or \"?\", _display(d),\n                d.get(\"ip\") or \"Unknown\"))\n        lines += [\"\", \"Выберете MAC для изменения имени.\"]\n        mac_buttons = []\n        for d in chunk:\n            mac = d.get(\"mac\") or \"\"\n            if not mac:\n                continue\n            mac_buttons.append(InlineKeyboardButton(\n                _display(d), callback_data=\"net_setname:%s\" % mac))\n        rows = [mac_buttons[i:i + 3] for i in range(0, len(mac_buttons), 3)]\n        nav = []\n        if page > 1:\n            nav.append(InlineKeyboardButton(\n                \"⬅️\", callback_data=\"net_status:%s\" % (page - 1)))\n        nav.append(InlineKeyboardButton(\"%s/%s\" % (page, total_pages),\n                                        callback_data=\"net_noop\"))\n        if page < total_pages:\n            nav.append(InlineKeyboardButton(\n                \"➡️\", callback_data=\"net_status:%s\" % (page + 1)))\n        rows.append(nav)\n        rows.append([InlineKeyboardButton(\"🔙 Назад\",\n                                          callback_data=\"net_menu\")])\n        self._send(context.bot, chat_id, \"\\n\".join(lines),\n                   InlineKeyboardMarkup(rows))\n\n    # -- статистика ------------------------------------------------------\n    def show_stats(self, update, context, page=1):\n        query = update.callback_query\n        try:\n            query.answer()\n        except Exception:  # noqa: BLE001\n            pass\n        user_id = update.effective_user.id\n        chat_id = update.effective_chat.id\n        if not self._can_view(user_id):\n            self._send(context.bot, chat_id, \"🚫 Доступ запрещён!\")\n            return\n        devs = self._devices()\n        if not devs:\n            self._send(context.bot, chat_id, \"Ошибка загрузки кэша сети.\")\n            return\n        online = sum(1 for d in devs if d.get(\"online\"))\n        text = (\"Всего устройств: %s · Онлайн: %s · Офлайн: %s\\n\\n\" % (\n            len(devs), online, len(devs) - online))\n        total_pages = max(1, (len(devs) + _PAGE - 1) // _PAGE)\n        page = max(1, min(page, total_pages))\n        chunk = devs[(page - 1) * _PAGE:page * _PAGE]\n        lines = [text]\n        for d in chunk:\n            st = \"🔄\" if d.get(\"online\") else \"🔌\"\n            lines.append(\"%s %s (%s)\" % (st, _display(d),\n                                         d.get(\"ip\") or \"Unknown\"))\n            lines.append(\"  коннектов: %s/%s · онлайн: %s\" % (\n                d.get(\"connections_today\", 0),\n                d.get(\"connections_total\", 0),\n                _fmt_online(d.get(\"online_seconds_total\", 0))))\n        if total_pages > 1:\n            lines.append(\"(стр. %s/%s)\" % (page, total_pages))\n        nav = []\n        if page > 1:\n            nav.append(InlineKeyboardButton(\n                \"⬅️\", callback_data=\"net_stats:%s\" % (page - 1)))\n        if page < total_pages:\n            nav.append(InlineKeyboardButton(\n                \"➡️\", callback_data=\"net_stats:%s\" % (page + 1)))\n        rows = [nav] if nav else []\n        rows.append([InlineKeyboardButton(\"🔙 Назад\",\n                                          callback_data=\"net_menu\")])\n        self._send(context.bot, chat_id, \"\\n\".join(lines),\n                   InlineKeyboardMarkup(rows))\n\n    # -- переименование ----------------------------------------------------\n    def ask_mac(self, update, context):\n        query = update.callback_query\n        try:\n            query.answer()\n        except Exception:  # noqa: BLE001\n            pass\n        user_id = update.effective_user.id\n        chat_id = update.effective_chat.id\n        if not self._can_edit(user_id):\n            self._send(context.bot, chat_id, \"🚫 Доступ запрещён!\")\n            return\n        context.chat_data[\"awaiting\"] = \"net_addname\"\n        self._send(context.bot, chat_id,\n                   \"Введите MAC адрес для которого хотите изменить имя:\")\n\n    def ask_name(self, update, context, mac):\n        query = update.callback_query\n        try:\n            query.answer()\n        except Exception:  # noqa: BLE001\n            pass\n        user_id = update.effective_user.id\n        chat_id = update.effective_chat.id\n        if not self._can_edit(user_id):\n            self._send(context.bot, chat_id, \"🚫 Доступ запрещён!\")\n            return\n        context.chat_data[\"awaiting\"] = \"net_setname\"\n        context.chat_data[\"mac_for_setname\"] = mac\n        self._send(context.bot, chat_id,\n                   \"Введите новое имя для MAC %s:\" % mac)\n\n    # -- игнор -------------------------------------------------------------\n    def show_ignore(self, update, context):\n        query = update.callback_query\n        try:\n            query.answer()\n        except Exception:  # noqa: BLE001\n            pass\n        user_id = update.effective_user.id\n        chat_id = update.effective_chat.id\n        if not self._can_edit(user_id):\n            self._send(context.bot, chat_id, \"🚫 Доступ запрещён!\")\n            return\n        try:\n            ignored = _store.load_ignored() or []\n        except Exception:  # noqa: BLE001\n            ignored = []\n        text = (\"🔇 Игнор-лист (%s):\\n%s\" % (\n            len(ignored),\n            \"\\n\".join(\"• %s\" % m for m in ignored) if ignored else \"— пусто —\"))\n        kb = InlineKeyboardMarkup([\n            [InlineKeyboardButton(\"➕ Добавить MAC\",\n                                  callback_data=\"net_ignore_add\")],\n            [InlineKeyboardButton(\"❌ Удалить MAC\",\n                                  callback_data=\"net_ignore_del\")],\n            [InlineKeyboardButton(\"🔙 Назад\", callback_data=\"net_menu\")],\n        ])\n        self._send(context.bot, chat_id, text, kb)\n\n    def ask_ignore(self, update, context, action):\n        query = update.callback_query\n        try:\n            query.answer()\n        except Exception:  # noqa: BLE001\n            pass\n        user_id = update.effective_user.id\n        chat_id = update.effective_chat.id\n        if not self._can_edit(user_id):\n            self._send(context.bot, chat_id, \"🚫 Доступ запрещён!\")\n            return\n        context.chat_data[\"awaiting\"] = (\"net_ignore_add\"\n                                         if action == \"add\"\n                                         else \"net_ignore_del\")\n        self._send(context.bot, chat_id,\n                   \"Введите MAC адрес для %s в игнор-лист:\" % (\n                       \"добавления\" if action == \"add\" else \"удаления\"))\n\n    # -- ввод текстом --------------------------------------------------------\n    def on_text(self, update, context):\n        state = context.chat_data.get(\"awaiting\")\n        if state not in (\"net_addname\", \"net_setname\", \"net_ignore_add\",\n                         \"net_ignore_del\"):\n            return\n        context.chat_data.pop(\"awaiting\", None)\n        try:\n            user_id = update.effective_user.id\n            chat_id = update.effective_chat.id\n            if not self._can_edit(user_id):\n                return\n            text = (update.message.text or \"\").strip()\n            if state == \"net_addname\":\n                if not re.match(_MAC_RE, text):\n                    self._send(context.bot, chat_id,\n                               \"❌ Некорректный MAC. Формат: AA:BB:CC:DD:EE:FF.\")\n                    return\n                context.chat_data[\"awaiting\"] = \"net_setname\"\n                context.chat_data[\"mac_for_setname\"] = text.upper()\n                self._send(context.bot, chat_id,\n                           \"Введите новое имя для MAC %s:\" % text.upper())\n                return\n            if state == \"net_setname\":\n                mac = context.chat_data.pop(\"mac_for_setname\", \"\")\n                if not mac or not text:\n                    self._send(context.bot, chat_id, \"❌ Отменено.\")\n                    return\n                ok, err = _store.set_device_name(mac, text[:64])\n                self._send(context.bot, chat_id,\n                           \"✅ Имя сохранено: %s\" % mac if ok\n                           else \"⚠️ %s\" % (err or \"ошибка\"))\n                return\n            action = \"add\" if state == \"net_ignore_add\" else \"remove\"\n            if not re.match(_MAC_RE, text):\n                self._send(context.bot, chat_id,\n                           \"❌ Некорректный MAC. Формат: AA:BB:CC:DD:EE:FF.\")\n                return\n            _, err = _store.set_ignored(action, text.upper())\n            self._send(context.bot, chat_id,\n                       \"✅ Игнор-лист обновлён.\" if not err\n                       else \"⚠️ %s\" % err)\n        finally:\n            # Не пускаем ввод в общий текстовый хендлер admin.\n            raise DispatcherHandlerStop()\n\n    # -- вход ------------------------------------------------------------------\n    def handle(self, update, context):\n        data = (update.callback_query.data or \"\")\n        # Правило меню: предыдущее затирается, новое отправляется.\n        # main_menu — дальше admin покажет главное (без стопа).\n        try:\n            context.bot.delete_message(\n                update.callback_query.message.chat_id,\n                update.callback_query.message.message_id)\n        except Exception:  # noqa: BLE001\n            pass\n        if data == \"main_menu\":\n            return\n        if data == \"net_noop\":\n            try:\n                update.callback_query.answer()\n            except Exception:  # noqa: BLE001\n                pass\n            return\n        if data in (\"net_menu\", \"net_status\"):\n            if data == \"net_menu\":\n                self.show_menu(update, context)\n            else:\n                self.show_status(update, context, 1)\n        elif data.startswith(\"net_status:\"):\n            try:\n                page = int(data.split(\":\", 1)[1])\n            except ValueError:\n                page = 1\n            self.show_status(update, context, page)\n        elif data in (\"net_stats\",):\n            self.show_stats(update, context, 1)\n        elif data.startswith(\"net_stats:\"):\n            try:\n                page = int(data.split(\":\", 1)[1])\n            except ValueError:\n                page = 1\n            self.show_stats(update, context, page)\n        elif data == \"net_addname\":\n            self.ask_mac(update, context)\n        elif data.startswith(\"net_setname:\"):\n            self.ask_name(update, context, data.split(\":\", 1)[1])\n        elif data.startswith(\"nmap_setname:\"):\n            # Легаси-кнопки из уведомлений сканера (scan.py): тот же\n            # диалог, что net_setname. Без этой ветки — «Неизвестная\n            # команда» из catch-all admin.\n            self.ask_name(update, context, data.split(\":\", 1)[1])\n        elif data == \"net_ignore\":\n            self.show_ignore(update, context)\n        elif data == \"net_ignore_add\":\n            self.ask_ignore(update, context, \"add\")\n        elif data == \"net_ignore_del\":\n            self.ask_ignore(update, context, \"del\")\n        # Не пускаем свои колбэки в catch-all admin.button (иначе\n        # «Неизвестная команда» поверх). main_menu — выше без стопа.\n        raise DispatcherHandlerStop()\n\n\ndef _fmt_online(sec):\n    try:\n        sec = int(sec or 0)\n    except (TypeError, ValueError):\n        return \"—\"\n    if sec < 60:\n        return \"%s сек\" % sec\n    m, s = divmod(sec, 60)\n    h, m = divmod(m, 60)\n    d, h = divmod(h, 24)\n    parts = []\n    if d:\n        parts.append(\"%s д\" % d)\n    if h:\n        parts.append(\"%s ч\" % h)\n    if m:\n        parts.append(\"%s мин\" % m)\n    return \" \".join(parts) or \"0 мин\"\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"network.tg — TG-меню сканирования (паритет с продом).</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Статус/статистика с пагинацией, кастомные имена MAC, игнор-лист.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Данные — devices_snapshot + store пакета (имена/игнор).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Просмотр — view_net_status, изменения — nmap_manage.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Legacy-регистрация в ui_tg (группа -1; ввод текста — группа -6).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> telegram </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardButton, InlineKeyboardMarkup</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> telegram.ext </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> DispatcherHandlerStop</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> store </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _store</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">logger </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> logging.getLogger(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">__name__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_VIEW_RIGHT</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"view_net_status\"</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_EDIT_RIGHT</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"nmap_manage\"</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_PAGE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 8</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_MAC_RE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">^([0-9A-Fa-f]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">{2}</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">[:-])</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">{5}</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">[0-9A-Fa-f]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">{2}</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">$</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _qid</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(uid):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    s </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(uid)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \":\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"telegram:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> s</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _display</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(d):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mac\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Unknown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">class</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> NetMenu</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> __init__</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, ctx):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _can_view</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, user_id):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx.rights.can(_qid(user_id), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_VIEW_RIGHT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _can_edit</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, user_id):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx.rights.can(_qid(user_id), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_EDIT_RIGHT</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> False</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _devices</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            snap </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network.devices_snapshot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network TG snapshot: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, e)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        devs </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> snap.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"devices\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> devs </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(d, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _send</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, bot, chat_id, text, reply_markup</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            bot.send_message(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">chat_id</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">chat_id, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">text</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">text,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                             reply_markup</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">reply_markup)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            logger.error(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network TG send: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, e)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # -- меню ---------------------------------------------------------</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> show_menu</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, update, context):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        query </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.callback_query</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            query.answer()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_user.id</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chat_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_chat.id</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._can_view(user_id):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        kb </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardMarkup([</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            [InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📊 Статус сети\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                                  callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_status\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            [InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📈 Статистика устройств\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                                  callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_stats\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            [InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📝 Изменить имя для MAC\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                                  callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_addname\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            [InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔇 Фильтр MAC\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                                  callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_ignore\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            [InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🏠 Главное меню\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                                  callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"main_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ])</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Меню сканирования:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, kb)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # -- статус --------------------------------------------------------</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> show_status</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, update, context, page</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        query </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.callback_query</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            query.answer()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_user.id</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chat_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_chat.id</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._can_view(user_id):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        devs </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._devices()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> devs:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка загрузки кэша сети.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        total_pages </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> max</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(devs) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _PAGE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> -</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">//</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _PAGE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> max</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">min</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(page, total_pages))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chunk </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> devs[(page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _PAGE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _PAGE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        header </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Обзор сети\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> total_pages </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            header </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" (стр. </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (page, total_pages)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        lines </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [header, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> chunk:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            st </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"🔄\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"🔌\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            lines.append(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> %s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">) - </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                st, d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mac\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"?\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, _display(d),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Unknown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        lines </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Выберете MAC для изменения имени.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mac_buttons </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> chunk:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mac\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                continue</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            mac_buttons.append(InlineKeyboardButton(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                _display(d), </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_setname:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        rows </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [mac_buttons[i:i </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> i </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> range</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(mac_buttons), </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        nav </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            nav.append(InlineKeyboardButton(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"⬅️\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_status:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        nav.append(InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (page, total_pages),</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                                        callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_noop\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> total_pages:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            nav.append(InlineKeyboardButton(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"➡️\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_status:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        rows.append(nav)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        rows.append([InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔙 Назад\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                                          callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)])</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(lines),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                   InlineKeyboardMarkup(rows))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # -- статистика ------------------------------------------------------</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> show_stats</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, update, context, page</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        query </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.callback_query</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            query.answer()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_user.id</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chat_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_chat.id</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._can_view(user_id):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        devs </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._devices()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> devs:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка загрузки кэша сети.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        online </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sum</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> devs </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        text </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Всего устройств: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> · Онлайн: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> · Офлайн: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s\\n\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(devs), online, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(devs) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> online))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        total_pages </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> max</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(devs) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _PAGE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> -</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">//</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _PAGE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> max</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">min</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(page, total_pages))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chunk </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> devs[(page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _PAGE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">*</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> _PAGE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        lines </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [text]</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> chunk:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            st </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"🔄\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"🔌\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            lines.append(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> %s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (st, _display(d),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                                         d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Unknown\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            lines.append(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"  коннектов: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> · онлайн: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connections_today\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connections_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                _fmt_online(d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online_seconds_total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> total_pages </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            lines.append(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"(стр. </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">/</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (page, total_pages))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        nav </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            nav.append(InlineKeyboardButton(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"⬅️\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_stats:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> total_pages:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            nav.append(InlineKeyboardButton(</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"➡️\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_stats:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        rows </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [nav] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> nav </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        rows.append([InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔙 Назад\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                                          callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)])</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(lines),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                   InlineKeyboardMarkup(rows))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # -- переименование ----------------------------------------------------</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ask_mac</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, update, context):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        query </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.callback_query</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            query.answer()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_user.id</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chat_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_chat.id</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._can_edit(user_id):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        context.chat_data[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"awaiting\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"net_addname\"</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                   \"Введите MAC адрес для которого хотите изменить имя:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ask_name</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, update, context, mac):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        query </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.callback_query</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            query.answer()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_user.id</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chat_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_chat.id</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._can_edit(user_id):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        context.chat_data[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"awaiting\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"net_setname\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        context.chat_data[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mac_for_setname\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                   \"Введите новое имя для MAC </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">:\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # -- игнор -------------------------------------------------------------</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> show_ignore</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, update, context):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        query </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.callback_query</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            query.answer()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_user.id</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chat_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_chat.id</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._can_edit(user_id):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ignored </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _store.load_ignored() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            ignored </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        text </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔇 Игнор-лист (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">):</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ignored),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">            \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"• </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ignored) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ignored </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"— пусто —\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        kb </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InlineKeyboardMarkup([</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            [InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"➕ Добавить MAC\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                                  callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_ignore_add\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            [InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"❌ Удалить MAC\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                                  callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_ignore_del\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            [InlineKeyboardButton(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔙 Назад\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">callback_data</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ])</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id, text, kb)</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ask_ignore</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, update, context, action):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        query </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.callback_query</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            query.answer()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        user_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_user.id</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chat_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_chat.id</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._can_edit(user_id):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🚫 Доступ запрещён!\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        context.chat_data[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"awaiting\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_ignore_add\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                                         if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> action </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"add\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                                         else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"net_ignore_del\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">        self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                   \"Введите MAC адрес для </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> в игнор-лист:\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                       \"добавления\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> action </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"add\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"удаления\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # -- ввод текстом --------------------------------------------------------</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> on_text</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, update, context):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        state </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> context.chat_data.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"awaiting\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_addname\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_setname\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_ignore_add\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                         \"net_ignore_del\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        context.chat_data.pop(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"awaiting\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            user_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_user.id</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            chat_id </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> update.effective_chat.id</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._can_edit(user_id):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            text </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (update.message.text </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"net_addname\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re.match(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_MAC_RE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, text):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                               \"❌ Некорректный MAC. Формат: AA:BB:CC:DD:EE:FF.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                context.chat_data[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"awaiting\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"net_setname\"</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                context.chat_data[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mac_for_setname\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> text.upper()</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                           \"Введите новое имя для MAC </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">:\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> text.upper())</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"net_setname\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> context.chat_data.pop(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mac_for_setname\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> text:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                    self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"❌ Отменено.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                    return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                ok, err </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _store.set_device_name(mac, text[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">64</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                           \"✅ Имя сохранено: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ok</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                           else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"⚠️ </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (err </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            action </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"add\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"net_ignore_add\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"remove\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re.match(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_MAC_RE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, text):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                           \"❌ Некорректный MAC. Формат: AA:BB:CC:DD:EE:FF.\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                return</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            _, err </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _store.set_ignored(action, text.upper())</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">._send(context.bot, chat_id,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                       \"✅ Игнор-лист обновлён.\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> err</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                       else</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"⚠️ </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> err)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        finally</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Не пускаем ввод в общий текстовый хендлер admin.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> DispatcherHandlerStop()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">    # -- вход ------------------------------------------------------------------</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> handle</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(self, update, context):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (update.callback_query.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Правило меню: предыдущее затирается, новое отправляется.</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # main_menu — дальше admin покажет главное (без стопа).</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            context.bot.delete_message(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                update.callback_query.message.chat_id,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                update.callback_query.message.message_id)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            pass</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"main_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"net_noop\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                update.callback_query.answer()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                pass</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_status\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"net_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_menu(update, context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            else</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">                self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_status(update, context, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_status:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\":\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_status(update, context, page)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_stats\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_stats(update, context, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_stats:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\":\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                page </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_stats(update, context, page)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"net_addname\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ask_mac(update, context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_setname:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ask_name(update, context, data.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\":\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data.startswith(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap_setname:\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # Легаси-кнопки из уведомлений сканера (scan.py): тот же</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # диалог, что net_setname. Без этой ветки — «Неизвестная</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">            # команда» из catch-all admin.</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ask_name(update, context, data.split(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\":\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">])</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"net_ignore\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.show_ignore(update, context)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"net_ignore_add\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ask_ignore(update, context, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"add\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        elif</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"net_ignore_del\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">            self</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.ask_ignore(update, context, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"del\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # Не пускаем свои колбэки в catch-all admin.button (иначе</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">        # «Неизвестная команда» поверх). main_menu — выше без стопа.</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> DispatcherHandlerStop()</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> _fmt_online</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(sec):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        sec </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> int</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(sec </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    except</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TypeError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ValueError</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"—\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sec </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 60</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> сек\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> sec</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    m, s </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> divmod</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(sec, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">60</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    h, m </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> divmod</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(m, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">60</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    d, h </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> divmod</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(h, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">24</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    parts </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        parts.append(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> д\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> h:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        parts.append(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> ч\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> h)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        parts.append(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> мин\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> m)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" \"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.join(parts) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"0 мин\"</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "ui_tg.py",
          "size": 1273,
          "content": "\"\"\"network.ui_tg — кнопка /start + legacy-хендлер меню сканирования.\"\"\"\n\nTG_COMMANDS = {}\n\nTG_MENU = [\n    {\"id\": \"netscan\", \"title\": \"📡 Сканирование\", \"callback\": \"net_menu\",\n     \"right\": \"view_net_status\"},\n]\n\n\ndef register_ptb(facade, mod):\n    \"\"\"Legacy-хук: меню сети на фасаде напрямую (колбэки net_*).\n\n    Группа -1: раньше catch-all admin.button (группа 0).\n    Ввод текста — группа -5 (НЕ -6: в PTB внутри группы срабатывает\n    только первый совпавший хендлер; в -6 уже сидит fail2ban.on_text,\n    и network.on_text за ним не вызывался бы никогда).\n    \"\"\"\n    from telegram.ext import CallbackQueryHandler, MessageHandler, Filters\n\n    from .tg import NetMenu\n    menu = NetMenu(mod.ctx)\n    mod.net_menu = menu\n    facade.add_handler(CallbackQueryHandler(\n        menu.handle,\n        pattern=(r\"^(net_menu|net_status|net_stats|net_addname|\"\n                 r\"net_setname:|net_ignore|net_noop|\"\n                 r\"main_menu|nmap_setname:)\")), group=-1)\n    facade.add_handler(MessageHandler(Filters.text, menu.on_text),\n                       group=-5)\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"network.ui_tg — кнопка /start + legacy-хендлер меню сканирования.\"\"\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TG_COMMANDS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">TG_MENU</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"netscan\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"📡 Сканирование\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"callback\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_menu\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"view_net_status\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> register_ptb</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(facade, mod):</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"Legacy-хук: меню сети на фасаде напрямую (колбэки net_*).</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Группа -1: раньше catch-all admin.button (группа 0).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    Ввод текста — группа -5 (НЕ -6: в PTB внутри группы срабатывает</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    только первый совпавший хендлер; в -6 уже сидит fail2ban.on_text,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    и network.on_text за ним не вызывался бы никогда).</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    \"\"\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> telegram.ext </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> CallbackQueryHandler, MessageHandler, Filters</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> .tg </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> NetMenu</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    menu </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> NetMenu(mod.ctx)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    mod.net_menu </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> menu</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    facade.add_handler(CallbackQueryHandler(</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        menu.handle,</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">        pattern</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">^(</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">net_menu</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">net_status</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">net_stats</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">net_addname</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                 r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">net_setname:</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">net_ignore</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">net_noop</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">                 r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">main_menu</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">|</span><span style=\"color:#032F62;--shiki-dark:#DBEDFF\">nmap_setname:)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)), </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">group</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">1</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    facade.add_handler(MessageHandler(Filters.text, menu.on_text),</span></span>\n<span class=\"line\"><span style=\"color:#E36209;--shiki-dark:#FFAB70\">                       group</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=-</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "ui_web.py",
          "size": 4602,
          "content": "\n\"\"\"network.ui_web — /api/network/devices + /api/admin/nmap*.\n\nПорт AdminNmap/Name/Ignore из старого messengers/web.py. ACL и traffic\nпереехали в модуль network_router. Пути кешей — через ctx.api network.\n\"\"\"\n\nROUTES = [\n    (\"GET\", \"/api/network/devices\", \"net_devices\", {\"right\": None,\n     \"desc\": \"Устройства сети для панели мониторинга (свои кеши)\"}),\n    (\"GET\", \"/api/admin/nmap\", \"nmap\", {\"right\": \"nmap_manage\",\n     \"desc\": \"Устройства сканера + игнор-лист\"}),\n    (\"POST\", \"/api/admin/nmap/name\", \"nmap_name\", {\"right\": \"nmap_manage\",\n     \"desc\": \"Кастомное имя {mac, name}\"}),\n    (\"POST\", \"/api/admin/nmap/ignore\", \"nmap_ignore\", {\"right\": \"nmap_manage\",\n     \"desc\": \"Игнор-лист {action: add|remove, mac}\"}),\n    (\"POST\", \"/api/network/nmap/delete\", \"nmap_delete\", {\"right\": \"nmap_manage\",\n     \"desc\": \"Удалить устройство из кеша {mac}\"}),\n]\n\nSERVICE = [\n    {\"key\": \"nmap_manage\", \"icon\": \"🔍\", \"title\": \"Сканер сети\", \"right\": \"nmap_manage\",\n     \"phase\": 3, \"desc\": \"Устройства, кастомные имена, игнор-лист\"},\n]\n\nPANELS = []\n\nMONITOR_PANELS = [\n    {\"id\": \"net.devices\", \"title\": \"Сеть: устройства\", \"api\": \"/api/network/devices\",\n     \"visibility\": \"public\", \"refresh_s\": 60},\n]\n\n_MAC_RE = r\"^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$\"\n\n\nfrom . import store as _store\n\ndef handle_api(ctx, config, method, req):\n    import re as _re\n\n    from core.errors import UserError\n\n    if method == \"net_devices\":\n        try:\n            data = ctx.api.call(\"network.devices_snapshot\")\n        except Exception as e:  # noqa: BLE001\n            return 500, {\"ok\": False, \"error\": str(e)[:200]}\n        if not isinstance(data, dict):\n            data = {\"total\": 0, \"online\": 0, \"devices\": []}\n        data[\"ok\"] = True\n        return data\n\n    from core.errors import UserError\n\n    if method == \"nmap\":\n        cache = _store.load_devices()\n        ignored = _store.load_ignored()\n        devices = []\n        for mac, d in cache.items():\n            devices.append({\n                \"mac\": mac, \"ip\": d.get(\"ip\"), \"name\": d.get(\"name\") or \"\",\n                \"name_custom\": d.get(\"name_custom\") or \"\",\n                \"type\": d.get(\"type\") or \"\", \"online\": bool(d.get(\"connected\")),\n                \"ports\": d.get(\"ports\") or [],\n                \"ignored\": mac.upper() in [x.upper() for x in ignored],\n            })\n        devices.sort(key=lambda x: (not x[\"online\"],\n                                    (x[\"name_custom\"] or x[\"name\"] or \"\").lower()))\n        return {\"devices\": devices, \"ignored\": ignored}\n\n    if method == \"nmap_name\":\n        body = req.get(\"body\") or {}\n        mac = (body.get(\"mac\") or \"\").strip().upper()\n        name = (body.get(\"name\") or \"\").strip()\n        if not _re.match(_MAC_RE, mac):\n            return 400, {\"ok\": False, \"error\": \"Некорректный MAC\"}\n        if len(name) > 64:\n            return 400, {\"ok\": False, \"error\": \"Имя слишком длинное (64)\"}\n        ok, err = _store.set_device_name(mac, name)\n        if not ok:\n            code = 400 if \"не найден\" in err else 500\n            return code, {\"ok\": False, \"error\": err[:400]}\n        return {\"ok\": True, \"mac\": mac, \"name\": name}\n\n    if method == \"nmap_ignore\":\n        body = req.get(\"body\") or {}\n        action = (body.get(\"action\") or \"\").strip()\n        mac = (body.get(\"mac\") or \"\").strip().upper()\n        if action not in (\"add\", \"remove\"):\n            return 400, {\"ok\": False, \"error\": \"action add/remove\"}\n        if not _re.match(_MAC_RE, mac):\n            return 400, {\"ok\": False, \"error\": \"Некорректный MAC\"}\n        ignored, err = _store.set_ignored(action, mac)\n        if err:\n            code = 400 if \"MAC \" in err or \"уже\" in err or \"нет\" in err else 500\n            return code, {\"ok\": False, \"error\": err[:400]}\n        return {\"ok\": True, \"ignored\": ignored}\n\n    if method == \"nmap_delete\":\n        body = req.get(\"body\") or {}\n        mac = (body.get(\"mac\") or \"\").strip().upper()\n        if not _re.match(_MAC_RE, mac):\n            return 400, {\"ok\": False, \"error\": \"Некорректный MAC\"}\n        ok, err = _store.delete_device(mac)\n        if not ok:\n            code = 400 if \"не найден\" in err else 500\n            return code, {\"ok\": False, \"error\": err[:400]}\n        return {\"ok\": True, \"mac\": mac}\n\n    raise UserError(\"неизвестный метод: %s\" % method)",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"network.ui_web — /api/network/devices + /api/admin/nmap*.</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">Порт AdminNmap/Name/Ignore из старого messengers/web.py. ACL и traffic</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">переехали в модуль network_router. Пути кешей — через ctx.api network.</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">ROUTES</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/network/devices\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net_devices\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">None</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Устройства сети для панели мониторинга (свои кеши)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"GET\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/nmap\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Устройства сканера + игнор-лист\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/nmap/name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap_name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Кастомное имя {mac, name}\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/nmap/ignore\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap_ignore\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Игнор-лист </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{action</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> add|remove, mac}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/network/nmap/delete\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap_delete\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Удалить устройство из кеша </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">{mac}</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">SERVICE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"key\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"icon\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"🔍\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Сканер сети\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"right\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"phase\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Устройства, кастомные имена, игнор-лист\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">PANELS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">MONITOR_PANELS</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"id\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net.devices\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"title\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Сеть: устройства\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"api\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/network/devices\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">     \"visibility\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"public\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"refresh_s\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">60</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">},</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_MAC_RE</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">^([0-9A-Fa-f]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">{2}</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">[:-])</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">{5}</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">[0-9A-Fa-f]</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">{2}</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">$</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> . </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> store </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _store</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">def</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> handle_api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx, config, method, req):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> re </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.errors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"net_devices\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        try</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.api.call(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"network.devices_snapshot\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        except</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> Exception</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> as</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> e:  </span><span style=\"color:#6A737D;--shiki-dark:#6A737D\"># noqa: BLE001</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">str</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(e)[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">200</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> isinstance</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(data, </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">dict</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"total\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"devices\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: []}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        data[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> True</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> data</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    from</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> core.errors </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">import</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"nmap\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        cache </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _store.load_devices()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ignored </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _store.load_ignored()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        devices </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac, d </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cache.items():</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            devices.append({</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"mac\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: mac, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ip\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">), </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"type\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">bool</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"connected\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)),</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"ports\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: d.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ports\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [],</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"ignored\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: mac.upper() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [x.upper() </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ignored],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            })</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        devices.sort(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">key</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=lambda</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x: (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"online\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">],</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">                                    (x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name_custom\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> x[</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).lower()))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"devices\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: devices, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ignored\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: ignored}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"nmap_name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mac\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip().upper()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re.match(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_MAC_RE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, mac):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Некорректный MAC\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> len</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(name) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">></span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 64</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Имя слишком длинное (64)\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ok, err </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _store.set_device_name(mac, name)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ok:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            code </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"не найден\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> err </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> code, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: err[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mac\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: mac, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: name}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"nmap_ignore\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        action </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"action\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip()</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mac\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip().upper()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> action </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">not</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"add\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"remove\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"action add/remove\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re.match(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_MAC_RE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, mac):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Некорректный MAC\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ignored, err </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _store.set_ignored(action, mac)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> err:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            code </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"MAC \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> err </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"уже\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> err </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"нет\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> err </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> code, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: err[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ignored\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: ignored}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"nmap_delete\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        body </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> req.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"body\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (body.get(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mac\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">or</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).strip().upper()</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _re.match(</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">_MAC_RE</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, mac):</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Некорректный MAC\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">        ok, err </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> _store.delete_device(mac)</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        if</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> not</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ok:</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">            code </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 400</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> if</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"не найден\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> err </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 500</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">            return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> code, {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">False</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"error\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: err[:</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">400</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">]}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">        return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"ok\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">True</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mac\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">: mac}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    raise</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> UserError(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"неизвестный метод: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">%s</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> %</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> method)</span></span></code></pre>"
        },
        {
          "path": "web/view.html",
          "size": 1084,
          "content": "<!-- Модульные вьюхи network. -->\n<section id=\"view-nmap\" class=\"hidden\">\n  <h2>Сканер сети</h2>\n  <p class=\"muted\">Устройства из <code>cache/networkscan_cache.json</code>. Требует <code>nmap_manage</code>. Кастомное имя — <code>name_custom</code>, игнор-лист — <code>cache/networkscan_ignored.json</code>.</p>\n  <div class=\"toolbar\">\n    <button id=\"nmap-refresh\" class=\"btn-mini\">Обновить</button>\n    <span class=\"muted\" id=\"nmap-info\"></span>\n  </div>\n  <div class=\"table-wrap\"><table id=\"nmap-table\"><thead><tr><th>MAC</th><th>IP / Имя</th><th>Тип</th><th>Статус</th><th>Действия</th></tr></thead><tbody></tbody></table></div>\n  <div class=\"card\" style=\"margin-top:12px\">\n    <h3>Игнор-лист</h3>\n    <p class=\"muted\" style=\"font-size:12px\">MAC из списка не попадают в сканирование и уведомления. Добавление — только для MAC из кэша.</p>\n    <pre id=\"nmap-ignored\" class=\"mono\"></pre>\n  </div>\n</section>\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">&#x3C;!-- Модульные вьюхи network. --></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">section</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"view-nmap\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"hidden\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Сканер сети&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h2</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Устройства из &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>cache/networkscan_cache.json&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>. Требует &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>nmap_manage&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>. Кастомное имя — &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>name_custom&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>, игнор-лист — &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>cache/networkscan_ignored.json&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">code</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>.&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"toolbar\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap-refresh\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"btn-mini\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Обновить&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">button</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap-info\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">span</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"table-wrap\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">table</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap-table\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">thead</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">tr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>MAC&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>IP / Имя&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Тип&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Статус&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Действия&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">th</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">tr</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">thead</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">tbody</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">tbody</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">table</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"card\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"margin-top:12px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>Игнор-лист&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">h3</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"muted\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> style</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"font-size:12px\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>MAC из списка не попадают в сканирование и уведомления. Добавление — только для MAC из кэша.&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    &#x3C;</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">pre</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> id</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"nmap-ignored\"</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> class</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"mono\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">>&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">pre</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  &#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">div</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">&#x3C;/</span><span style=\"color:#22863A;--shiki-dark:#85E89D\">section</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">></span></span>\n<span class=\"line\"></span></code></pre>"
        },
        {
          "path": "web/view.js",
          "size": 6135,
          "content": "/* network — модульные вьюхи (modules/network/web/view.js). */\n/* ---------------- сканер сети (nmap_manage) ---------------- */\n\nasync function loadNmap() {\n  if (!state.me || !(state.me.is_admin || state.me.rights.nmap_manage)) { showToast(\"Нет права nmap_manage\"); showView(\"dashboard\"); return; }\n  const r = await api(\"/api/admin/nmap\");\n  const tb = $(\"#nmap-table tbody\");\n  if (r.status === 401 || r.status === 403) { tb.innerHTML = `<tr><td colspan=\"5\" class=\"muted\">Нет доступа</td></tr>`; return; }\n  if (!r.ok) { tb.innerHTML = `<tr><td colspan=\"5\" class=\"muted\">Ошибка: ${esc(r.data.error||\"\")}</td></tr>`; return; }\n  const devs = r.data.devices || [];\n  const ignored = r.data.ignored || [];\n  tb.innerHTML = devs.length ? devs.map((d)=> `\n    <tr>\n      <td class=\"mono\">${esc(d.mac)}</td>\n      <td>${esc(d.name_custom || d.name || \"—\")}<br><span class=\"muted\">${esc(d.ip||\"\")}</span>${d.ignored ? ' <span class=\"pill off\">игнор</span>' : ''}</td>\n      <td class=\"muted\">${esc(d.type||\"\")}</td>\n      <td>${pill(d.online,\"онлайн\",\"офлайн\")}</td>\n      <td class=\"actions\"><div style=\"display:flex;gap:6px;flex-wrap:wrap\">\n        <button class=\"btn-mini\" onclick=\"nmapSetName(${escJs(d.mac)})\">Имя</button>\n        ${d.ignored ? `<button class=\"btn-mini\" onclick=\"nmapIgnore('remove','${esc(d.mac)}')\">Убрать игнор</button>` : `<button class=\"btn-mini\" onclick=\"nmapIgnore('add','${esc(d.mac)}')\">Игнор</button>`}\n        <button class=\"btn-mini\" onclick=\"nmapDel(${escJs(d.mac)})\" title=\"Удалить запись из кеша (вернётся при следующем скане, если в сети)\">🗑</button>\n      </div></td>\n    </tr>`).join(\"\") : `<tr><td colspan=\"5\" class=\"muted\">устройств нет</td></tr>`;\n  $(\"#nmap-info\").textContent = `устройств: ${devs.length} · игнор: ${ignored.length}`;\n  $(\"#nmap-ignored\").textContent = ignored.length ? ignored.join(\"\\n\") : \"пусто\";\n}\nasync function nmapSetName(mac) {\n  const cur = prompt(`Новое имя для ${mac} (пусто — сброс, #cancel — отмена):`, \"\");\n  if (cur === null) return;\n  const name = cur.trim();\n  const r = await api(\"/api/admin/nmap/name\", { method:\"POST\", body: JSON.stringify({mac, name}) });\n  if (r.data.ok) { showToast(\"Имя сохранено\"); loadNmap(); refreshMonitoring(); } else showToast(r.data.error||\"Ошибка\");\n}\nasync function nmapIgnore(action, mac) {\n  if (!confirm(`${action===\"add\"?\"Добавить\":\"Убрать\"} ${mac} ${action===\"add\"?\"в игнор\":\"из игнора\"}?`)) return;\n  const r = await api(\"/api/admin/nmap/ignore\", { method:\"POST\", body: JSON.stringify({action, mac}) });\n  if (r.data.ok) { showToast(\"OK\"); loadNmap(); } else showToast(r.data.error||\"Ошибка\");\n}\nasync function nmapDel(mac) {\n  if (!confirm(`Удалить ${mac} из кеша? (вернётся при следующем скане, если устройство в сети)`)) return;\n  const r = await api(\"/api/network/nmap/delete\", { method:\"POST\", body: JSON.stringify({mac}) });\n  if (r.data && r.data.ok) { showToast(\"Удалено\"); loadNmap(); refreshMonitoring(); } else showToast((r.data && r.data.error)||\"Ошибка\");\n}\n$(\"#nmap-refresh\")?.addEventListener(\"click\", () => loadNmap());\n\nwindow.BotUI.registerService('nmap_manage', 'nmap');\nwindow.BotUI.registerView('nmap', loadNmap);\n\n/* Рендеры мониторинга (network). */\nconst netSort = { key: \"status\", dir: \"desc\" };\n\nfunction renderNetTable() {\n  markSorted(\"net-table\", netSort);\n  const keyFn = (d) => ({\n    name: (d.name || \"\").toLowerCase(),\n    ip: d.ip || \"\",\n    ports: (d.ports || []).length,\n    status: d.online ? 1 : 0,\n    duration: d.duration_s || 0,\n    last_change: d.last_change || \"\",\n    offline: d.offline_since || \"\",\n  }[netSort.key]);\n  const rows = sortRows(state.netDevices, netSort, keyFn);\n  const tbody = $(\"#net-table tbody\");\n  tbody.innerHTML = rows.map((d) => {\n    const portsTxt = d.ports && d.ports.length\n      ? (d.ports.length === 1 && d.ports[0] === \"Нет открытых портов\" ? \"—\" : esc(d.ports.join(\", \")))\n      : \"—\";\n    const stateWord = d.online ? \"онлайн\" : \"оффлайн\";\n    const dur = d.duration_str && d.duration_str !== \"—\" ? esc(d.duration_str) : \"—\";\n    const last = d.last_change ? esc(d.last_change) : \"—\";\n    const canRename = state.me && (state.me.is_admin || (state.me.rights && state.me.rights.nmap_manage));\n    const renameBtn = canRename\n      ? ` <a href=\"#\" onclick=\"return netRename(${escJs(d.mac)},'${esc(d.name)}')\" title=\"Переименовать\" style=\"text-decoration:none\">✏️</a>` : \"\";\n    return `\n    <tr>\n      <td>${esc(d.name)}${renameBtn}<br><span class=\"muted mono\" style=\"font-size:11px\">${esc(d.mac)}</span></td>\n      <td class=\"mono\">${esc(d.ip || \"—\")}</td>\n      <td class=\"mono muted\" style=\"font-size:11px;max-width:160px;overflow:hidden;text-overflow:ellipsis\">${portsTxt}</td>\n      <td>${pill(d.online)}<br><span class=\"muted\" style=\"font-size:11px;line-height:1.35\">${stateWord} ${dur}<br>с ${last}</span></td>\n    </tr>`;\n  }).join(\"\") || `<tr><td colspan=\"4\" class=\"muted\">нет данных сканера</td></tr>`;\n}\n\nfunction renderNetDevices(net) {\n  net = net || {};\n  const sum = $(\"#net-summary\");\n  if (sum) sum.textContent = net.total != null\n    ? `· онлайн ${net.online}/${net.total}` : \"\";\n  state.netDevices = net.devices || [];\n  renderNetTable();\n}\n\nasync function netRename(mac, cur) {\n  const name = prompt(\"Новое имя для \" + mac + \" (пусто — сбросить):\", cur || \"\");\n  if (name === null) return false;\n  const r = await api(\"/api/admin/nmap/name\", { method: \"POST\", body: JSON.stringify({ mac, name: name.trim() }) });\n  if (r.ok && r.data.ok) { showToast(\"Имя сохранено\"); refreshMonitoring(); }\n  else showToast((r.data && r.data.error) || \"Ошибка\");\n  return false;\n}\n\nwindow.BotUI.registerPanelRenderer('net.devices', (el, p, d) => renderNetDevices(d));\n",
          "contentHtml": "<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* network — модульные вьюхи (modules/network/web/view.js). */</span></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* ---------------- сканер сети (nmap_manage) ---------------- */</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> loadNmap</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">state.me </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(state.me.is_admin </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state.me.rights.nmap_manage)) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Нет права nmap_manage\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"dashboard\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/nmap\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> tb</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#nmap-table tbody\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (r.status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 401</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.status </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 403</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) { tb.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;tr>&#x3C;td colspan=\"5\" class=\"muted\">Нет доступа&#x3C;/td>&#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r.ok) { tb.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;tr>&#x3C;td colspan=\"5\" class=\"muted\">Ошибка: ${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">r</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">data</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">error</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td>&#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">; }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> devs</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.devices </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> ignored</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.ignored </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  tb.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> devs.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> devs.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">d</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    &#x3C;tr></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td class=\"mono\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mac</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td>${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name_custom</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"—\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;br>&#x3C;span class=\"muted\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ip</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ignored</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> ' &#x3C;span class=\"pill off\">игнор&#x3C;/span>'</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> ''}&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td class=\"muted\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">type</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td>${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">pill</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">online</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">,</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"онлайн\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">,</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"офлайн\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td class=\"actions\">&#x3C;div style=\"display:flex;gap:6px;flex-wrap:wrap\"></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        &#x3C;button class=\"btn-mini\" onclick=\"nmapSetName(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mac</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})\">Имя&#x3C;/button></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ignored</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;button class=\"btn-mini\" onclick=\"nmapIgnore('remove','${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mac</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}')\">Убрать игнор&#x3C;/button>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;button class=\"btn-mini\" onclick=\"nmapIgnore('add','${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mac</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}')\">Игнор&#x3C;/button>`}</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">        &#x3C;button class=\"btn-mini\" onclick=\"nmapDel(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mac</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">})\" title=\"Удалить запись из кеша (вернётся при следующем скане, если в сети)\">🗑&#x3C;/button></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;/div>&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    &#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;tr>&#x3C;td colspan=\"5\" class=\"muted\">устройств нет&#x3C;/td>&#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#nmap-info\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `устройств: ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">devs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} · игнор: ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ignored</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#nmap-ignored\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ignored.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ignored.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"пусто\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> nmapSetName</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">mac</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> cur</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> prompt</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">`Новое имя для ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mac</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} (пусто — сброс, #cancel — отмена):`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (cur </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> name</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> cur.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">trim</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/nmap/name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({mac, name}) });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (r.data.ok) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Имя сохранено\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadNmap</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">refreshMonitoring</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); } </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(r.data.error</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> nmapIgnore</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">action</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">mac</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">confirm</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">`${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">action</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"add\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Добавить\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Убрать\"} ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mac</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">action</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"add\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"в игнор\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"из игнора\"}?`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/nmap/ignore\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({action, mac}) });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (r.data.ok) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"OK\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadNmap</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); } </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(r.data.error</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> nmapDel</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">mac</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">confirm</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">`Удалить ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mac</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} из кеша? (вернётся при следующем скане, если устройство в сети)`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/network/nmap/delete\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({mac}) });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.ok) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Удалено\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">loadNmap</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">refreshMonitoring</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); } </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error)</span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">$</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#nmap-refresh\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)?.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">addEventListener</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"click\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, () </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> loadNmap</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">());</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">window.BotUI.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">registerService</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'nmap_manage'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'nmap'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">window.BotUI.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">registerView</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'nmap'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, loadNmap);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6A737D;--shiki-dark:#6A737D\">/* Рендеры мониторинга (network). */</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> netSort</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> { key: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"status\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, dir: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"desc\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> renderNetTable</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  markSorted</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"net-table\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, netSort);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> keyFn</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">d</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ({</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    name: (d.name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">toLowerCase</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(),</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    ip: d.ip </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    ports: (d.ports </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> []).</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    status: d.online </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    duration: d.duration_s </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    last_change: d.last_change </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">    offline: d.offline_since </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">,</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }[netSort.key]);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> rows</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> sortRows</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(state.netDevices, netSort, keyFn);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> tbody</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#net-table tbody\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  tbody.innerHTML </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> rows.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">map</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">d</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> portsTxt</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.ports </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.ports.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">      ?</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (d.ports.</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">length</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ===</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> &#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.ports[</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">] </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Нет открытых портов\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"—\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> esc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(d.ports.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\", \"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">)))</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">      :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"—\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> stateWord</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.online </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"онлайн\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"оффлайн\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> dur</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.duration_str </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.duration_str </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!==</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"—\"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ?</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> esc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(d.duration_str) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"—\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> last</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> d.last_change </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> esc</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(d.last_change) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">:</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"—\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> canRename</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state.me </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (state.me.is_admin </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (state.me.rights </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> state.me.rights.nmap_manage));</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> renameBtn</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> canRename</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">      ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> ` &#x3C;a href=\"#\" onclick=\"return netRename(${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">escJs</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mac</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">},'${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}')\" title=\"Переименовать\" style=\"text-decoration:none\">✏️&#x3C;/a>`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    return</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `</span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    &#x3C;tr></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td>${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">name</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">renameBtn</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;br>&#x3C;span class=\"muted mono\" style=\"font-size:11px\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">mac</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td class=\"mono\">${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">esc</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">ip</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> ||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"—\"</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td class=\"mono muted\" style=\"font-size:11px;max-width:160px;overflow:hidden;text-overflow:ellipsis\">${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">portsTxt</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">      &#x3C;td>${</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">pill</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">(</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">d</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">online</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">)</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;br>&#x3C;span class=\"muted\" style=\"font-size:11px;line-height:1.35\">${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">stateWord</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">} ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">dur</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;br>с ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">last</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}&#x3C;/span>&#x3C;/td></span></span>\n<span class=\"line\"><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">    &#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  }).</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">join</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `&#x3C;tr>&#x3C;td colspan=\"4\" class=\"muted\">нет данных сканера&#x3C;/td>&#x3C;/tr>`</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> renderNetDevices</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">net</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  net </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> net </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> sum</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> $</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"#net-summary\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (sum) sum.textContent </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> net.total </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">    ?</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> `· онлайн ${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">net</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">online</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}/${</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">net</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">.</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">total</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">}`</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> :</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  state.netDevices </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> net.devices </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">  renderNetTable</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">();</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">async</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> function</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> netRename</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">mac</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">cur</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> name</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> prompt</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Новое имя для \"</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> mac </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">+</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \" (пусто — сбросить):\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, cur </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (name </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">===</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  const</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> r</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"color:#D73A49;--shiki-dark:#F97583\"> await</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> api</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"/api/admin/nmap/name\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, { method: </span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"POST\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, body: </span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\">JSON</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">stringify</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">({ mac, name: name.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">trim</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">() }) });</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  if</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (r.ok </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.ok) { </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"Имя сохранено\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">); </span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">refreshMonitoring</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); }</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  else</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> showToast</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">((r.data </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> r.data.error) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">||</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"Ошибка\"</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">);</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">  return</span><span style=\"color:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">;</span></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">window.BotUI.</span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\">registerPanelRenderer</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"color:#032F62;--shiki-dark:#9ECBFF\">'net.devices'</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, (</span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">el</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">p</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">, </span><span style=\"color:#E36209;--shiki-dark:#FFAB70\">d</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">) </span><span style=\"color:#D73A49;--shiki-dark:#F97583\">=></span><span style=\"color:#6F42C1;--shiki-dark:#B392F0\"> renderNetDevices</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">(d));</span></span>\n<span class=\"line\"></span></code></pre>"
        }
      ],
      "note": "Модуль проверки локальной сети",
      "_extra": {
        "api": "",
        "events_emitted": "",
        "events_subscribed": "[]",
        "audit_allow": "",
        "uses_rights": "",
        "shared_routes": "",
        "config_schema": "config.schema.json",
        "env_example": ".env.example"
      }
    }
  ]
}