Продукты Р7
Корпоративный сервер 2024
Корпоративный сервер 2024
Сервер документов
Сервер документов
Редакторы
Редакторы
Корпоративный сервер 2019
Корпоративный сервер 2019
Графика
Графика
Команда
Команда
Мобильные редакторы
Мобильные редакторы
Облачный офис
Облачный офис
Почта
Почта
Органайзер
Органайзер
Дополнительно
Часто задаваемые вопросы
Разработчикам
Интеграции
Новые возможности

Унифицированная инструкция для администраторов, как делать «плагины-встройки» для Корпоративного сервера 2024

Обновлено: 28.01.26

0. Что именно называется «плагином» в этой методичке

Речь не про «официальные» расширения, а про админскую встройку в веб-интерфейс:

  • В веб-интерфейс (SPA) добавляются свои JS-скрипты (инжекты), подключённые в index.html.
  • Эти скрипты:
    • добавляют элементы UI (вкладки, карточки, пункты меню),
    • делают запросы к локальному API на сервере (HTTP/WebSocket).
  • На сервере поднимается локальный микросервис (Node.js или Python) через systemd.
  • nginx проксирует URL вида /имя-плагина/... на этот микросервис (на 127.0.0.1:<порт>).

Именно так описаны и устроены примеры (например, SSL Monitor: backend ssl-status.js, ssl-change.js, UI “вкладка SSL”, nginx проксирует /ssl-status и /ssl-change).

1. С самого начала: как вообще работает веб-часть нашего продукта «Корпоративный сервер 2024»

1.1. Модули — это отдельные “мини-сайты”

Корпоративный сервер 2024 состоит из модулей (admin, calendar, cddisk, cdmail, contacts, projects, pages, forms). Конфигурации этих модулей лежат в /etc/nginx/sites-available/, и именно там правятся доменные имена/сертификаты для каждого модуля.

В конфиге nginx для модуля обычно есть:

  • root /var/www/r7-office/;
  • index index.html;

То есть каждый модуль — отдельная папка со статикой:

  • /var/www/r7-office/admin
  • /var/www/r7-office/calendar
  • /var/www/r7-office/cddisk
  • … и т.д.

1.2. Почему «достаточно править index.html»

Интерфейс — это Single Page Application:

  • браузер запрашивает index.html,
  • он подтягивает JS/CSS из assets/,
  • дальше приложение само переключает страницы без полной перезагрузки (меняется location.pathname, DOM перерисовывается).

Следствие: если в index.html добавить

<script src="/assets/inject-...js"></script>

, этот код начнёт выполняться при каждом открытии модуля, и сможет «прицепиться» к нужным частям интерфейса.

2. Большая картинка: кто с кем взаимодействует (архитектура)

Ниже — типовая схема для таких плагинов:

(Браузер администратора)
|
| 1) GET https://admin.домен/ -> index.html
| 2) GET /assets/... -> основная SPA
| 3) GET /assets/inject-*.js -> ваш “плагин” (frontend)
|
v
(nginx модуля admin / calendar / ...)
|
| 4) location /my-plugin/ { proxy_pass http://127.0.0.1:40850/; }
v
(backend-плагина на localhost, systemd)
|
| 5) читает логи/конфиги/выполняет команды (openssl, ldap и т.п.)
v
(файлы/службы ОС)

Идея разделения ролей:

  • Frontend-инжект отвечает за UI и запросы.
  • Backend-микросервис отвечает за “серверные” действия (чтение файлов, запуск команд, перезапуск сервисов).
  • nginx — мост между ними, чтобы запросы шли тем же доменом/схемой (без CORS и без открытия порта наружу).

3. Что даёт такой подход (возможности)

Такие встройки превращают админку в «панель управления инфраструктурой», не меняя исходники «Корпоративного сервер 2024»:

3.1. Мониторинг прямо в интерфейсе

Примеры по аналогии:

  • срок действия SSL и цепочка сертификатов (как SSL Monitor),
  • состояние синхронизации LDAP/AD (как LDAP Monitor),
  • прогресс фоновых задач/очередей/переиндексации (как Reindex Monitor).

3.2. Управляющие действия (кнопки «сделать»)

Возможны действия уровня:

  • запустить/остановить задачу,
  • очистить ошибки/повторить обработку,
  • перезапустить компонент (nginx, rabbitmq и др.),
  • применить конфиг (пример — сервис замены сертификата).
Обратите внимание

Любые «изменяющие» действия должны быть сделаны безопасно (см. раздел про безопасность).

3.3. «Мастера» и интеграции

Можно сделать UI-мастер, который:

  • собирает параметры,
  • валидирует,
  • отправляет на backend,
  • backend меняет конфиги, делает backup, делает nginx -t, перезагружает сервис.

4. Из каких частей состоит плагин-встройка (разбор по слоям)

4.1. Frontend-слой: inject-скрипты

Обычно это 2–3 файла:

  • inject-manager.js — общий «движок вкладок/панелей»
  • Он добавляет новые пункты меню в раздел настроек и умеет показывать/прятать «панели» плагинов.
  • Он экспортирует window.TabManager.register(...).

В примере менеджер следит за SPA-навигацией и при заходе на /settings ждёт «стабильного» меню, после чего вставляет ссылки и управляет активностью вкладок. (Именно поэтому менеджер должен грузиться раньше остальных инжектов.)

  • inject—tab.js — вкладка плагина (UI в настройках)
  • Регистрируется через TabManager.register({ id, title, buildPanel, onShow })
  • При показе делает fetch() к API или подгружает HTML.
  • inject—card.js — карточка/виджет на главной
  • Через DOM-поиск/MutationObserver вставляет карточку и обновляет её polling’ом или WebSocket.

4.2. Backend-слой: микросервис

Он нужен потому что:

  • в браузере нельзя читать /etc/nginx/..., /var/log/... и запускать openssl, systemctl, ldapsearch и т.п.
  • это всё должно происходить на сервере.

В примерах:

  • Reindex Monitor требует python3 + venv/pip и aiohttp (backend на Python/aiohttp).
  • LDAP Monitor требует nodejs (версия ≥ 10, рекомендовано 16+).
  • SSL Monitor аналогично на Node.js.

4.3. Связь frontend ↔ backend: nginx proxy

В nginx добавляется location, который проксирует URL-префикс плагина на localhost.

Пример из официального описания SSL Monitor: nginx проксирует /ssl-status /ssl-change.

4.4. Жизненный цикл: systemd

backend запускается как сервис:

  • стартует вместе с сервером,
  • перезапускается при падении,
  • имеет логи.

5. Почему «inject-manager.js должен быть первым» (простое объяснение)

Вкладки плагина выглядят так:

  • inject-manager.js создаёт глобальный объект window.TabManager.
  • inject-ldap-tab.js / inject-ssl-tab.js / inject-reindex-tab.js вызывают TabManager.register(...).

Если вкладка загрузится раньше менеджера — будет ошибка TabManager is not defined.

Поэтому установщики плагинов:

  • либо добавляют менеджер,
  • либо проверяют, что он уже есть,
  • и гарантируют порядок подключения в index.html.

Ниже указан код inject-manager.js, который можно использовать в своих плагинах.

(function () {
  'use strict';
 
  /* =======================
     CONFIG
     ======================= */
 
  const CONTENT_ROOT_SELECTOR = '.css-15wdyfr';
  const NAV_LINK_SELECTOR = 'a.mantine-NavLink-root';
  const SETTINGS_PATH = '/settings';
 
  /* =======================
     STATE
     ======================= */
 
  const state = {
    activeTab: null,
    panels: new Map(),
    registered: new Map(),
    originalDisplaySaved: false,
    lastPath: location.pathname,
    lastSettingsRoot: null
  };
 
  /* =======================
     HELPERS
     ======================= */
 
  function debounce(fn, wait) {
    let t;
    return function debounced(...args) {
      clearTimeout(t);
      t = setTimeout(() => fn.apply(this, args), wait);
    };
  }
 
  function getContentContainer() {
    return document.querySelector(CONTENT_ROOT_SELECTOR);
  }
 
  function getSystemIntegrationsLink() {
    return document.querySelector(
      `${NAV_LINK_SELECTOR}[href="${SETTINGS_PATH}"]`
    );
  }
 
  function clearActiveMenu() {
    document
      .querySelectorAll(NAV_LINK_SELECTOR)
      .forEach(a => a.removeAttribute('data-active'));
  }
 
  function setActiveMenu(link) {
    clearActiveMenu();
    if (link) {
      link.setAttribute('data-active', 'true');
    }
  }
 
  function resetForNewSettingsRoot() {
    state.originalDisplaySaved = false;
    state.activeTab = null;
 
    state.panels.forEach(panel => panel.remove());
    state.panels.clear();
 
    document
      .querySelectorAll(`${NAV_LINK_SELECTOR}[data-r7-tab]`)
      .forEach(a => a.remove());
  }
 
  function waitForStableSettingsMenu(cb) {
    let lastCount = 0;
    let stableTicks = 0;
 
    const timer = setInterval(() => {
      if (!location.pathname.startsWith(SETTINGS_PATH)) {
        clearInterval(timer);
        return;
      }
 
      const root = getContentContainer();
      const links = document.querySelectorAll(NAV_LINK_SELECTOR);
      const systemLink = getSystemIntegrationsLink();
 
      if (!root || !systemLink) {
        stableTicks = 0;
        return;
      }
 
      // новый DOM settings
      if (state.lastSettingsRoot !== root) {
        state.lastSettingsRoot = root;
        resetForNewSettingsRoot();
        stableTicks = 0;
        lastCount = 0;
        return;
      }
 
      if (links.length === lastCount) {
        stableTicks++;
      } else {
        stableTicks = 0;
        lastCount = links.length;
      }
 
      if (stableTicks >= 2) {
        clearInterval(timer);
        cb();
      }
    }, 100);
  }
 
  // Portal can re-render the settings page or the left menu on window resize,
  // detach tab, etc. In that case injected links disappear. We keep the menu
  // "alive" by re-binding registered tabs after DOM mutations.
  const ensureInjectedTabs = debounce(() => {
    if (!location.pathname.startsWith(SETTINGS_PATH)) return;
    waitForStableSettingsMenu(() => {
      state.registered.forEach(bindMenu);
      // If the active tab link was recreated, re-mark it active.
      if (state.activeTab) {
        setActiveMenu(document.querySelector(`[data-r7-tab="${state.activeTab.id}"]`));
      }
    });
  }, 150);
 
  function saveOriginalContentState() {
    if (state.originalDisplaySaved) return;
 
    const container = getContentContainer();
    if (!container) return;
 
    Array.from(container.children).forEach(child => {
      if (child.dataset.r7Panel) return;
 
      if (child.dataset.r7OrigDisplay === undefined) {
        child.dataset.r7OrigDisplay = child.style.display || '';
      }
    });
 
    state.originalDisplaySaved = true;
  }
 
  function hideOriginalContent(exceptPanel) {
    const container = getContentContainer();
    if (!container) return;
 
    Array.from(container.children).forEach(child => {
      if (child === exceptPanel) return;
      if (child.dataset.r7Panel) return;
      child.style.display = 'none';
    });
  }
 
  function restoreOriginalContent() {
    const container = getContentContainer();
    if (!container) return;
 
    Array.from(container.children).forEach(child => {
      if (child.dataset.r7Panel) return;
 
      child.style.display =
        child.dataset.r7OrigDisplay !== undefined
          ? child.dataset.r7OrigDisplay
          : '';
    });
  }
 
  /* =======================
     MENU
     ======================= */
 
  function buildMenuLink(tab) {
    const existing = document.querySelector(`[data-r7-tab="${tab.id}"]`);
    if (existing) return existing;
 
    const systemLink = getSystemIntegrationsLink();
    if (!systemLink) return null;
 
    const link = document.createElement('a');
    link.href = '#';
    link.className = systemLink.className;
    link.dataset.r7Tab = tab.id;
 
    link.innerHTML = `
      <span class="mantine-NavLink-section" style="margin-inline-end: var(--mantine-spacing-sm);">
        ${tab.iconHTML || ''}
      </span>
      <div class="mantine-NavLink-body">
        <span class="mantine-NavLink-label">${tab.title}</span>
      </div>
    `;
 
    systemLink.insertAdjacentElement('afterend', link);
    return link;
  }
 
  function bindMenu(tab) {
    const link = buildMenuLink(tab);
    if (!link || link.dataset.r7Bound) return;
 
    link.dataset.r7Bound = '1';
    link.addEventListener('click', e => {
      e.preventDefault();
      showTab(tab);
    });
  }
 
  /* =======================
     CORE
     ======================= */
 
  function hideAllCustomPanels() {
    state.panels.forEach(panel => {
      panel.style.display = 'none';
    });
  }
 
  function hideActiveTab() {
    state.activeTab?.onHide?.();
    hideAllCustomPanels();
    restoreOriginalContent();
    clearActiveMenu();
    state.activeTab = null;
  }
 
  function showTab(tab) {
    if (state.activeTab?.id === tab.id) return;
 
    const container = getContentContainer();
    if (!container) return;
 
    hideActiveTab();
    saveOriginalContentState();
 
    let panel = state.panels.get(tab.id);
    if (!panel) {
      panel = tab.buildPanel();
      if (!panel) return;
 
      panel.dataset.r7Panel = tab.id;
      panel.style.display = 'none';
      state.panels.set(tab.id, panel);
      container.appendChild(panel);
    }
 
    hideOriginalContent(panel);
    panel.style.display = 'block';
 
    setActiveMenu(document.querySelector(`[data-r7-tab="${tab.id}"]`));
 
    tab.onShow?.();
    state.activeTab = tab;
  }
 
  /* =======================
     PUBLIC API
     ======================= */
 
  function register(tab) {
    if (!tab?.id || !tab?.title || typeof tab.buildPanel !== 'function') {
      throw new Error('TabManager.register: invalid config');
    }
 
    state.registered.set(tab.id, tab);
 
    if (location.pathname.startsWith(SETTINGS_PATH)) {
      waitForStableSettingsMenu(() => bindMenu(tab));
    }
  }
 
  /* =======================
     GLOBAL LISTENERS
     ======================= */
 
  document.addEventListener(
    'click',
    e => {
      const link = e.target.closest?.(NAV_LINK_SELECTOR);
      if (!link) return;
 
      if (link.dataset.r7Tab) return;
 
      hideActiveTab();
      setActiveMenu(link);
    },
    true
  );
 
  /* =======================
     RESILIENCE (DOM RE-RENDER)
     ======================= */
 
  // Many SPA frameworks fully remount the settings layout on resize / detach,
  // which removes our injected links. Watch the DOM and re-inject.
  try {
    const mo = new MutationObserver(() => ensureInjectedTabs());
    mo.observe(document.documentElement, {
      childList: true,
      subtree: true
    });
  } catch (_) {
    // ignore (MutationObserver not available)
  }
 
  window.addEventListener('resize', () => ensureInjectedTabs(), { passive: true });
 
  /* =======================
     SPA PATH WATCHER
     ======================= */
 
  setInterval(() => {
    const cur = location.pathname;
    const prev = state.lastPath;
 
    if (cur === prev) return;
 
    if (!prev.startsWith(SETTINGS_PATH) &&
        cur.startsWith(SETTINGS_PATH)) {
      waitForStableSettingsMenu(() => {
        state.registered.forEach(bindMenu);
      });
    }
 
    if (prev.startsWith(SETTINGS_PATH) &&
        !cur.startsWith(SETTINGS_PATH)) {
      hideActiveTab();
    }
 
    state.lastPath = cur;
  }, 200);
 
  /* =======================
     EXPORT
     ======================= */
 
  window.TabManager = {
    register,
    hideActiveTab
  };
})();

6. Унифицированные соглашения (чтобы 100 админов делали одинаково)

Чтобы такие плагины можно было поддерживать на большой аудитории, вводятся единые правила.

6.1. Нейминг

Пусть <PLUGIN> — короткое имя, например my-plugin.

  • Каталог backend:
    /opt/r7-/-<PLUGIN>/
  • systemd unit:
    r7-<PLUGIN>.service (или несколько, если нужно)
  • nginx-блок:
    # BEGIN R7_<PLUGIN_UPPER># END R7_<PLUGIN_UPPER>
  • URL-префикс:
    /r7-<PLUGIN>/ (рекомендуется единый неймспейс, чтобы не конфликтовать с будущими роутами продукта)

6.2. Единые точки встраивания по модулю

Для выбранного модуля <module>:

  • JS кладётся в:
    /var/www/r7-office/<module>/assets/
  • Скрипты подключаются в:
    /var/www/r7-office/<module>/index.html
  • Прокси добавляется в:
    /etc/nginx/sites-available/<module>

Модули (типовой набор): admin, calendar, cddisk, cdmail, contacts, projects, pages, forms.

6.3. Единый «паспорт изменений» (для аудита)

Каждый плагин должен документировать, что он меняет. Например, SSL Monitor в официальной инструкции перечисляет затрагиваемые пути:

  • /opt/...
  • /etc/systemd/system/*.service
  • /etc/nginx/sites-available/admin
  • /var/www/r7-office/admin/assets/*.js
  • /var/www/r7-office/admin/index.html
    и отмечает, что для критичных файлов создаются .bak-копии.

Это правильный стандарт для всех.

7. Пошаговое создание нового плагина (от идеи до удаления)

Шаг 1. Сформулировать задачу и выбрать тип UI

Варианты UI-встройки:

  1. Вкладка в настройках (как SSL/LDAP/Reindex) — хорошо для “панелей управления”.
  2. Карточка на главной — хорошо для “быстрых статусов”.
  3. Отдельная страница плагина (отдавать HTML через backend + вставлять ссылку).

На старте проще всего: “вкладка + API”.

Шаг 2. Выбрать модуль, куда встраиваться

По умолчанию все три примера ориентируются на admin, потому что там админские настройки.

Но встраиваться можно и в другие модули — там просто будет другой DOM/другие страницы. Подход по файлам тот же (assets + index.html + nginx конфиг модуля).

Шаг 3. Спроектировать URL-префикс и порт

Пример “унифицировано”:

  • URL: /r7-my-plugin/
  • Внутренний порт backend: 40850 (любой свободный, проверяется ss)
Обратите внимание

URL и nginx-location должны совпадать с тем, что использует frontend.

Шаг 4. Написать backend-микросервис

4.1. Минимальный контракт API

Нормальный стартовый контракт:

  • GET /api/status → JSON со статусом
  • (опционально) GET /ws → WebSocket для пуш-обновлений
  • (опционально) POST /api/action → действие (перезапуск/обновление и т.п.)

4.2. Почему backend лучше слушать 127.0.0.1

Так порт не торчит наружу; доступ идёт только через nginx по основному домену.

Шаг 5. Создать systemd unit для backend

Задача unit-файла:

  • стартовать сервис,
  • держать его живым,
  • писать лог в journal.

На больших инсталляциях полезно добавлять hardening (ограничения прав), но для тестирования достаточно Restart=always.

Шаг 6. Настроить nginx proxy (сразу правильно)

6.1. HTTP proxy

В конфиге nginx модуля <module> добавить:

# BEGIN R7_MY_PLUGIN
location /r7-my-plugin/ {
    proxy_pass http://127.0.0.1:40850/;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
}
# END R7_MY_PLUGIN

6.2. Если нужен WebSocket

Тогда обязательно:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;

(Именно так сделано в примерах, где используются WebSocket/долгие соединения.)

Шаг 7. Написать frontend-инжект (вкладка)

7.1. Основная идея вкладки

  1. buildPanel() создаёт DOM-контейнер.
  2. onShow() загружает данные через fetch('/r7-my-plugin/api/status').
  3. TabManager.register(...) сообщает менеджеру, что вкладка существует.

7.2. Важное для SPA

SPA может перерисовывать DOM. Поэтому код должен:

  • уметь “ждать” появления нужного контейнера,
  • не вставлять UI по 10 раз,
  • переживать переходы по страницам.

Шаг 8. Подключить инжекты в index.html

Вставить в /var/www/r7-office/<module>/index.html:

<script src="/assets/inject-manager.js"></script>
<script src="/assets/inject-my-plugin-tab.js"></script>
<script src="/assets/inject-my-plugin-card.js"></script>
Важно

Строгое правило: manager раньше остальных.

Шаг 9. Единый install.sh: установка, проверка, восстановление и удаление (одним скриптом)

9.1. Зачем вообще нужен install.sh (и почему он должен быть одинаковым)

Если плагин ставится “руками”, на 10–100 серверах неизбежно появятся:

  • разные пути, разные порты, разные правки index.html;
  • дубли <script>, конфликтующие location в nginx;
  • отсутствие резервных копий → невозможно нормально откатиться.

Поэтому у каждого плагина должен быть один скрипт, который:

  1. всегда делает одинаковые изменения (воспроизводимость);
  2. не ломается при повторном запуске (идемпотентность);
  3. оставляет следы (backup + маркеры BEGIN/END + лог);
  4. умеет install / uninstall / check / repair.

9.2. Стандарт интерфейса (CLI) для всех плагинов

Унификация начинается с того, как запускается установщик:

./install.sh install   --module admin --port 40850
./install.sh check     --module admin
./install.sh repair    --module admin
./install.sh uninstall --module admin

Правила:

  • --module — куда встраивается UI (admin, calendar, projects …).
  • --port — внутренний порт backend (только localhost).
  • Если --module не задан — по умолчанию admin.
  • Если --port не задан — берётся из диапазона свободных портов или из env.

9.3. Инварианты (что install.sh обязан гарантировать)

Независимо от «темы» плагина, install.sh делает одни и те же типы действий:

A) UI-слой (веб-модуль):

  • кладёт inject-*.js в /var/www/r7-office/<module>/assets/;
  • добавляет <script src="/assets/inject-...js"></script> в /var/www/r7-office/<module>/index.html;
  • гарантирует порядок: inject-manager.js раньше вкладок (если используется TabManager).

B) Backend-слой (ОС):

  • раскладывает backend в /opt/r7-<plugin>/;
  • создаёт systemd unit r7-<plugin>.service и включает сервис.

C) Связь (nginx):

  • добавляет location /r7-<plugin>/ в /etc/nginx/sites-available/<module>;
  • отмечает блок маркерами # BEGIN ... # END ... для гарантированного удаления.

D) Безопасность и поддержка:

  • делает .bak критичных файлов перед изменением (index.html, nginx-конфиг);
  • пишет лог установки;
  • поддерживает check и repair.

9.4. В установщик обязательно заложить «самовосстановление» UI после обновлений (guard + ExecStartPre)

При создании плагина нужно исходить из реальности эксплуатации: после обновления Корпоративного сервера часто перезаписываются веб-файлы модулей (например, index.html и содержимое assets/). В результате:

  • исчезают ваши inject-*.js из /var/www/r7-office/<module>/assets/;
  • пропадают строки <script src="/assets/inject-...js"></script> из /var/www/r7-office/<module>/index.html;
  • backend-сервис продолжает работать, но вкладка/карточка в интерфейсе пропадает, потому что фронтенд больше не подключён.

Чтобы плагин был “живучим” и не требовал ручной переустановки на каждом обновлении, установщик должен сразу создавать механизм проверки и восстановления (guard) и подключать его в systemd.

Что именно делает «guard»-механизм

Установщик должен реализовать 3 обязательных элемента:

      1. Резервная копия UI-компонентов (источник восстановления)

  • При установке (или при первом запуске guard) формируется каталог:
    • /opt/r7-<plugin>/backup/
  • Туда складываются копии всех файлов, которые должны жить в модуле:
    • inject-manager.js (если используется TabManager)
    • inject-<plugin>-tab.js
    • inject-<plugin>-card.js (если есть карточка)
    • любые css/svg/json, если они нужны плагину

      2. Скрипт проверки и восстановления

  • Создаётся исполняемый скрипт, например:
    • /opt/r7-<plugin>/ensure-<module>-assets.sh
      или универсальный вариант ensure-assets.sh --module <module>
  • Его задача при каждом запуске:
    • проверить наличие каждого файла в /var/www/r7-office/<module>/assets/;
    • если файла нет — восстановить из /opt/r7-<plugin>/backup/;
    • проверить index.html:
      • есть ли <script src="/assets/inject-manager.js"> и <script src="/assets/inject-<plugin>...">;
      • если тегов нет — добавить обратно (обычно перед </body>);
    • (опционально) логировать, что именно восстановлено.

      3. Подключение guard в systemd перед стартом сервиса
В unit-файл backend-сервиса нужно добавить:

ExecStartPre=/opt/r7-<plugin>/ensure-<module>-assets.sh

Смысл: каждый старт или рестарт backend-сервиса автоматически «чинит» фронтенд-встройку, если она пропала.

Почему именно через ExecStartPre

  • Это срабатывает автоматически: после обновлений часто делают рестарт сервисов/узла, и восстановление произойдёт без ручных действий.
  • Не нужен отдельный cron/timer.
  • Логи восстановления попадают в journalctl -u r7-<plugin> вместе с логами сервиса — удобно для аудита.

Набор обязательных правил для guard (чтобы не ломал систему)

  • Идемпотентность: повторный запуск не должен дублировать <script> и не должен “портить” index.html.
  • Безопасность: guard восстанавливает только из своего backup, не скачивает ничего извне.
  • Надёжность: если какие-то пути временно недоступны, guard не должен валить запуск сервиса без крайней необходимости (лучше “предупредить и продолжить”, чем оставить backend неработающим).
  • Порядок скриптов: если используется inject-manager.js, он должен быть добавлен раньше вкладок, иначе вкладки могут не зарегистрироваться.

Шаг 10. Сделать uninstall (обязательно)

Удаление должно делать обратное:

  1. systemctl stop/disable сервис(ы).
  2. Удалить unit-файлы из /etc/systemd/system/.
  3. systemctl daemon-reload.
  4. Удалить backend-директорию /opt/r7-<PLUGIN>/.
  5. Удалить JS-файлы плагина из assets/.
  6. Удалить строки <script ...inject-<PLUGIN>...> из index.html.
  7. Удалить nginx-блок по маркерам BEGIN/END, затем:
    • nginx -t
    • systemctl reload nginx

Отдельный вопрос: удалять ли inject-manager.js?

На большой инфраструктуре manager — общий для многих вкладок. Поэтому правило такое:

  • По умолчанию manager не удаляется, даже если удаляется один плагин.
  • Удаление manager допустимо только если точно известно, что других вкладок-плагинов больше нет.

8. Как встраивать не только в admin (важное уточнение)

Да, встроить можно в любые директории модулей (/var/www/r7-office/admin|calendar|...) и соответствующие nginx-конфиги (/etc/nginx/sites-available/admin|calendar|...).

Но есть тонкость:

  • “менеджер вкладок” чаще всего привязан к конкретному DOM и маршрутам.
  • Например, в admin есть /settings, меню Mantine и т.п.
    В других модулях меню может быть другим.

Практическое правило для унификации:

  • либо использовать вкладки только в admin,
  • либо делать “менеджер-адаптер”: конфиг селекторов/путей для каждого модуля.

9. Безопасность (обязательный раздел)

Такие плагины работают “внутри админки”, но backend — это настоящий сервис на сервере, который может:

  • читать конфиги,
  • выполнять команды,
  • перезапускать службы.

Минимальные правила безопасности:

  1. Слушать только 127.0.0.1 (не 0.0.0.0).
  2. Доступ к API — только через nginx домена модуля.
  3. Для “опасных действий” (POST, изменение файлов):
    • проверять метод,
    • проверять Content-Type,
    • добавлять простую защиту от случайного вызова (например, “одноразовый токен” в UI или обязательный подтверждающий параметр),
    • делать backup перед изменениями,
    • валидировать входные данные,
    • nginx -t перед reload.

10. Типовые ошибки новичков и как их предотвращать

1. Несовпадение URL в frontend и nginx

  • UI ходит на /r7-my-plugin/..., а nginx проксирует /my-plugin/... → 404.
  • Решение: единый константный URL_PREFIX во всех местах.

2. manager подключён после вкладки

  • Итог: TabManager is not defined.
  • Решение: установщик всегда упорядочивает <script>.

3. DOM-селекторы “сломались” после обновления

  • Решение: использовать более устойчивые точки, минимум “css-xxxxx” классов, добавлять fallback-поиск, делать режим --check-only.

4. После обновления R7 файл index.html переписан

  • Решение: хранить пакет плагина и переустанавливать/перепатчивать после обновлений; иметь “проверку наличия инжектов”.

11. Мини-шаблон «идеального» пакета плагина для распространения

Рекомендуемая структура:

r7-my-plugin/
install.sh
uninstall.sh (или install.sh --uninstall)
data/
inject-manager.js (общий, ставится при отсутствии)
inject-my-plugin-tab.js
inject-my-plugin-card.js
backend/
my-plugin.js (или app.py)
index.html (если отдаётся HTML-страница)
...

12. Контрольные команды (для тестов)

После установки:

  • Сервисы:
    • systemctl status r7-my-plugin
    • journalctl -u r7-my-plugin -n 100 --no-pager
  • Порт:
    • ss -lntp | grep <port>
  • nginx:
    • nginx -t
    • systemctl reload nginx
  • API:
  • В браузере:
    • DevTools → Network: грузятся ли /assets/inject-...
    • DevTools → Console: ошибки TabManager/fetch/CORS