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

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

Обновлено: 10.07.26

Инструкция актуальна для версии продукта: 2025.3.12.15877

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:
    • curl -k https://admin.домен/r7-my-plugin/api/status
  • В браузере:
    • DevTools → Network: грузятся ли /assets/inject-...
    • DevTools → Console: ошибки TabManager/fetch/CORS

Была ли эта статья полезной?

Поделитесь пожалуйста своим мнением

      В статье есть ошибкиРекомендации не помоглиТекст трудно понятьСодержание статьи не соответствует заголовкуДругое