Содержание
- 0. Что именно называется «плагином» в этой методичке
- 1. С самого начала: как вообще работает веб-часть нашего продукта «Корпоративный сервер 2024»
- 2. Большая картинка: кто с кем взаимодействует (архитектура)
- 3. Что даёт такой подход (возможности)
- 4. Из каких частей состоит плагин-встройка (разбор по слоям)
- 5. Почему «inject-manager.js должен быть первым» (простое объяснение)
- 6. Унифицированные соглашения (чтобы 100 админов делали одинаково)
- 7. Пошаговое создание нового плагина (от идеи до удаления)
- Шаг 1. Сформулировать задачу и выбрать тип UI
- Шаг 2. Выбрать модуль, куда встраиваться
- Шаг 3. Спроектировать URL-префикс и порт
- Шаг 4. Написать backend-микросервис
- Шаг 5. Создать systemd unit для backend
- Шаг 6. Настроить nginx proxy (сразу правильно)
- Шаг 7. Написать frontend-инжект (вкладка)
- Шаг 8. Подключить инжекты в index.html
- Шаг 9. Единый install.sh: установка, проверка, восстановление и удаление (одним скриптом)
- Шаг 10. Сделать uninstall (обязательно)
- 8. Как встраивать не только в admin (важное уточнение)
- 9. Безопасность (обязательный раздел)
- 10. Типовые ошибки новичков и как их предотвращать
- 11. Мини-шаблон «идеального» пакета плагина для распространения
- 12. Контрольные команды (для тестов)
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-встройки:
- Вкладка в настройках (как SSL/LDAP/Reindex) — хорошо для “панелей управления”.
- Карточка на главной — хорошо для “быстрых статусов”.
- Отдельная страница плагина (отдавать 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. Основная идея вкладки
buildPanel()создаёт DOM-контейнер.onShow()загружает данные черезfetch('/r7-my-plugin/api/status').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; - отсутствие резервных копий → невозможно нормально откатиться.
Поэтому у каждого плагина должен быть один скрипт, который:
- всегда делает одинаковые изменения (воспроизводимость);
- не ломается при повторном запуске (идемпотентность);
- оставляет следы (backup + маркеры BEGIN/END + лог);
- умеет
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>/; - создаёт
systemdunitr7-<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.jsinject-<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 (обязательно)
Удаление должно делать обратное:
systemctl stop/disableсервис(ы).- Удалить unit-файлы из
/etc/systemd/system/. systemctl daemon-reload.- Удалить backend-директорию
/opt/r7-<PLUGIN>/. - Удалить JS-файлы плагина из
assets/. - Удалить строки
<script ...inject-<PLUGIN>...>изindex.html. - Удалить nginx-блок по маркерам
BEGIN/END, затем:nginx -tsystemctl 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 — это настоящий сервис на сервере, который может:
- читать конфиги,
- выполнять команды,
- перезапускать службы.
Минимальные правила безопасности:
- Слушать только 127.0.0.1 (не 0.0.0.0).
- Доступ к API — только через nginx домена модуля.
- Для “опасных действий” (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-pluginjournalctl -u r7-my-plugin -n 100 --no-pager
- Порт:
ss -lntp | grep <port>
- nginx:
nginx -tsystemctl reload nginx
- API:
- В браузере:
- DevTools → Network: грузятся ли
/assets/inject-... - DevTools → Console: ошибки
TabManager/fetch/CORS
- DevTools → Network: грузятся ли








