---
title: Руководство для админа по TaigaDocs
sidebar_position: 4

doc_rev: l1kupec
doc_rev_of: fd421667e60431029fedaadfbc6e74bd0e7e3d2715c11e163f70fafe16d86670
doc_rev_at: "2026-09-28"

keywords: [l1kupec]
---
# Документация Taiga IoT — зона администратора
## Инструкция по серверу, GitLab, CI и сабмодулям

Внутренний сайт (Admin): **https://95.183.13.164.sslip.io:3002/**  
Страница входа: **https://auth.95.183.13.164.sslip.io:3002/**  
Публичный сайт (User): **http://95.183.13.164:3003/**

GitLab: **https://gitlab.taigaiot.com/**  
Umbrella: **https://gitlab.taigaiot.com/taiga_docs/taigadocs/taigadocs-internal** (project ID **177**)

Гайд для технического писателя:

[guide-to-OBSIDIAN-for-technical-writers](guide-to-OBSIDIAN-for-technical-writers.md)

---

## 1. Назначение системы

TaigaDocs — публикация технической документации двумя сайтами с одного набора статей.

Документация продукта лежит в своём GitLab-репозитории. В репозитории сайта эти продукты подключены как **git submodules** (вложенные репозитории). Путь submodule зеркалит путь проекта в GitLab: `docs/<CI_PROJECT_PATH>`. Например, репозиторий `taiga_docs/taigabadgelw/docs` лежит в umbrella как `docs/taiga_docs/taigabadgelw/docs`. Docusaurus собирает их в один сайт.

Техпис работает только с репозиторием продукта и Obsidian. Сервер, общий репозиторий и публичный сайт ему не нужны.

```text
Техпис
  ↓ Commit + Push в репозиторий продукта
Product CI
  ↓ переписывает картинки/PDF Obsidian
  ↓ trigger API
GitLab: TaigaDocs (project 177)
  ↓ sync_submodule — путь docs/<TRIGGERED_BY>, запоминает SHA
  ↓ deploy_docs
Docker на 95.183.13.164
  ↓
Admin  https://95.183.13.164.sslip.io:3002/   (сразу)
User   http://95.183.13.164:3003/             (только после тега docs-*)
```

**Рабочая папка администратора на ПК:** `C:/taiga-docs`  
Это клон родительского репозитория сайта (umbrella). Техпис клонирует не её, а репозиторий продукта.

---

## 2. Словарь

| Термин | Что это |
| --- | --- |
| **Umbrella** | Родительский репозиторий `taiga_docs/taigadocs/taigadocs-internal`. В нём сайт, CI и список продуктов |
| **Product** | Отдельный репозиторий продукта с Markdown |
| **Submodule** | Репозиторий продукта, подключённый внутрь umbrella |
| **Path-mirror** | Правило пути: submodule = `docs/` + путь проекта в GitLab (`CI_PROJECT_PATH`) |
| **Gitlink** | Запись в umbrella: «этот продукт зафиксирован на таком-то SHA» |
| **SHA** | Идентификатор одного коммита |
| **CI** | Автоматические действия GitLab после push |
| **Pipeline** | Один запуск CI |
| **Job** | Одна задача внутри pipeline |
| **Runner** | Машина, которая выполняет jobs. У нас это сам сервер сайта |
| **Docusaurus** | Сборщик статического сайта из Markdown |
| **Authelia** | Страница логина и сессии внутреннего сайта |
| **doc_rev** | Короткий номер версии текста статьи. Его считает сборка сайта |
| **Allowlist** | Список путей, которые можно показать на публичном сайте (`allowlist-public.yml`) |
| **Тег `docs-*`** | Метка на коммите umbrella. Только она обновляет публичный сайт |
| **Orphan** | Файл-вложение в папке статей, на который ни одна статья не ссылается |

---

# Часть A. Карта системы

## A1. Два сайта

| | Admin | User |
| --- | --- | --- |
| Зачем | Вся документация для своих. Сюда попадает push техписа | То, что уже решили показать наружу |
| Адрес | `https://95.183.13.164.sslip.io:3002/` | `http://95.183.13.164:3003/` |
| Вход | Логин Authelia | Нет |
| Откуда текст | ветка `main` umbrella | последний тег `docs-*` |
| Каталог на сервере | `/opt/docusaurus-docs` | `/opt/docusaurus-docs-user` |
| Репозиторий | `taiga_docs/taigadocs/taigadocs-internal` | тот же umbrella; публичный каталог собирается из шаблона + allowlist |

Push техписа меняет только Admin. User в этот момент остаётся на прошлом теге. Это не поломка.

Сертификат Admin самоподписанный (в образ его кладёт Dockerfile, в имени сертификата есть IP и `95.183.13.164.sslip.io`). Браузер показывает предупреждение — для этой лаборатории это нормально. Позже, когда будет свой домен, сертификат меняется на Let's Encrypt.

Почему не голый IP в адресной строке: сессия Authelia хранится в cookie, а cookie с доменом на голый IP не сажается. `sslip.io` — временное имя, которое само указывает на этот IP. Когда появится домен, в конфиге меняются два имени: сайт и `auth.…`.

Кнопка **Log out** в шапке ведёт на  
`https://auth.95.183.13.164.sslip.io:3002/logout?rd=…`  
и сбрасывает сессию. Старый адрес `http://IP:3002/` документацию больше не отдаёт.

TLS внутреннего сайта висит на **3002 → 443** внутри контейнера.

## A2. Репозитории GitLab

Umbrella: `taiga_docs/taigadocs/taigadocs-internal` — **project ID 177**

Документация продуктов живёт в группе `taiga_docs` (и при необходимости в других группах). Живой список submodule — файл `.gitmodules`. Если эта таблица и `.gitmodules` разошлись, верьте `.gitmodules`.

| Репозиторий GitLab | Путь submodule |
| --- | --- |
| `taiga_docs/taigabadgelw/docs` | `docs/taiga_docs/taigabadgelw/docs` |
| `taiga_docs/toesnode/docs` | `docs/taiga_docs/toesnode/docs` |
| `taiga_docs/taigabadgelw/software` | `docs/products/taigabadgelw/Software` |
| `taiga_docs/taigabadgelw/firmware` | `docs/products/taigabadgelw/Firmware` |
| `taiga_docs/toesnode/software` | `docs/products/toesnode/Software` |
| `taiga_docs/toesnode/firmware` | `docs/products/toesnode/Firmware` |
| `taiga_docs/group/test` | `docs/taiga_docs/group/test` |
| `taiga_docs/group/test-files` | `docs/taiga_docs/group/test-files` |
| `taiga_docs/group/test-permission` | `docs/taiga_docs/group/test-permission` |
| `taiga-products/ng_test_product` | `docs/taiga-products/ng_test_product` |

**Как читать пути.** Для новых репозиториев с документацией правило одно: путь в umbrella = `docs/` + путь проекта в GitLab. Старые Software/Firmware пока ещё лежат под `docs/products/...`. Sync при следующем trigger сам переносит submodule на path-mirror, если он был зарегистрирован по старому пути.

## A3. Что лежит в umbrella

```text
taigadocs-internal/
├── docs/                              ← статьи самого сайта + submodule продуктов
│   ├── taiga_docs/.../                ← path-mirror (docs/<gitlab path>)
│   └── products/.../                  ← часть старых Software/Firmware
├── .gitmodules
├── .gitlab-ci.yml
├── allowlist-public.yml               ← что можно показать на User
├── ci/product-trigger.gitlab-ci.yml
├── authelia/configuration.yml         ← логин; users_database.yml на сервере, не в git
├── docker-compose.yml                 ← Admin: docs + authelia + redis + history-api
├── docker-compose.lockdown.yml        ← рудемент
├── templates/taiga-docs-user/         ← шаблон публичного сайта
└── scripts/
    ├── ci-sync-submodule.sh
    ├── ci-deploy-compose.sh
    ├── ci-deploy-public.sh
    ├── convert-obsidian-embeds.sh
    ├── bump-doc-revs.cjs
    ├── prepare-docs-build.cjs
    ├── copy-raw-docs.cjs
    ├── generate-files-list-sidebar.cjs
    ├── check-doc-images.sh
    ├── check-doc-links.cjs
    ├── sanitize-svg.sh
    └── backup-admin-stack.sh
```

`ci-sync-submodule.sh` сам строит путь `docs/<TRIGGERED_BY>`. Отдельный `case` на каждый продукт не нужен. Если репозитория ещё нет в `.gitmodules`, sync может добавить его сам. Если репозиторий в GitLab удалён (HTTP 404), sync может убрать мёртвый submodule.

## A4. Сервер

| Параметр | Значение |
| --- | --- |
| IP | `95.183.13.164` |
| Имя для cookie | `95.183.13.164.sslip.io` |
| Каталог Admin | `/opt/docusaurus-docs` |
| Каталог User | `/opt/docusaurus-docs-user` |
| Runner | shell с тегом `docs_server_instance` на этом же VPS |
| Диск | около 59 ГБ. Кэшу Docker больше 2–4 ГБ не оставлять |

Контейнеры Admin (`docker compose ps` в `/opt/docusaurus-docs`):

| Контейнер                       | Снаружи                                  | Зачем                                               |
| ------------------------------- | ---------------------------------------- | --------------------------------------------------- |
| `docusaurus-docs-docs-1`        | `3002 → 443`                             | nginx, статика, TLS, проверка входа, прокси истории |
| `docusaurus-docs-authelia-1`    | нет, порт 9091 только внутри сети Docker | логин и сессии                                      |
| `docusaurus-docs-redis-1`       | нет                                      | сессии Authelia в памяти, на диск не пишет          |
| `docusaurus-docs-history-api-1` | нет                                      | история статьи из GitLab                            |

Публичный сайт — отдельный compose в `/opt/docusaurus-docs-user`, контейнер на `3003 → 80`, без Authelia.

Как устроен запрос к Admin:

```text
Браузер
  │  HTTPS :3002, имя *.sslip.io
  ▼
nginx в контейнере docs
  │  «этот человек уже вошёл?»
  ▼
Authelia
  │  сессия лежит в Redis
  ▼
nginx отдаёт страницу Docusaurus
```

Пароль Authelia сверяет с хешем в `authelia/users_database.yml`. Redis нужен только как список уже начатых сессий.

Секреты — в `/opt/docusaurus-docs/.env`, **не в git**:

| Переменная | Зачем |
| --- | --- |
| `AUTHELIA_JWT_SECRET` | служебные токены Authelia |
| `AUTHELIA_SESSION_SECRET` | подпись сессии |
| `AUTHELIA_STORAGE_ENCRYPTION_KEY` | шифрование хранилища Authelia |
| `DOCS_HISTORY_TOKEN` | чтение GitLab для кнопки истории. Без него деплой останавливается |

Первый пользователь `admin` и эти три ключа Authelia, если их ещё нет:

```bash
cd /opt/docusaurus-docs
bash scripts/authelia-bootstrap.sh 'ВашПароль'
```

`users_database.yml` на сервере есть, в git его нет. Не коммитьте его и не копируйте пароль в эту инструкцию.

Перед сменой портов, TLS, Authelia или lockdown сначала снимок:

```bash
cd /opt/docusaurus-docs
bash scripts/backup-admin-stack.sh pre-change
# → /root/backups/taiga-docs-admin/<дата>-pre-change/
```

Ручная пересборка, если нужно повторить то, что делает CI:

```bash
cd /opt/docusaurus-docs
bash scripts/ci-deploy-compose.sh
```

Скрипт не делает `docker rm`, а потом долгую сборку. Он сначала собирает образ, потом освобождает имена `docusaurus-docs-*-1`, потом запускает уже собранное. Если удалить контейнеры и только затем собирать, имя может занять другой процесс, и `up` упадёт с «container name already in use».

Проверка:

```bash
docker compose ps
```

В конце скрипт печатает, что Authelia поднята, и напоминает адрес `https://95.183.13.164.sslip.io:3002/`.

:::warning 
Диск и кэш сборки
CI крутится на этом же VPS. Каждая сборка образа пишет кэш BuildKit (часто 150–400 МБ). Без лимита за несколько дней диск 59 ГБ заканчивается, хотя «живые» контейнеры маленькие.

Лимит уже стоит в двух местах: `docker builder prune --keep-storage=2GB` внутри `scripts/ci-deploy-compose.sh` и GC в `/etc/docker/daemon.json` (`builder.gc.defaultKeepStorage`: `2GB`). Ручной `docker builder prune -af` без лимита диск освободит один раз; следующий день бампов набьёт его снова.

Крупное на диске, кроме Docker: `/root/backups` (старые дампы), MySQL `taiga_projects`, `/root/.vscode-server`. Хостовый `node_modules` в `/opt/docusaurus-docs` для работы сайта не нужен: сайт собирается в Docker.
:::

Закрыть Admin от интернета, оставив его только на сервере:

```bash
docker compose -f docker-compose.yml -f docker-compose.lockdown.yml up -d
```

Тогда порт 3002 слушает `127.0.0.1`. Снаружи его нет, с сервера и через SSH-туннель — есть. Cookie всё равно требует имя `*.sslip.io`, не `127.0.0.1`.

---

## A5. Логин и пароль

Логин есть только у TaigaAdmin (`https://95.183.13.164.sslip.io:3002/`).  
Публичный сайт `http://95.183.13.164:3003/` пароль не спрашивает.

Страницу входа рисует Authelia. nginx сайта сам статью не отдаёт: сначала спрашивает Authelia, есть ли у браузера живая сессия. Если нет — браузер уходит на страницу авторизации `https://auth.95.183.13.164.sslip.io:3002/`.

Пароль в открытом виде нигде не хранится. В файле на сервере лежит хеш (argon2). Redis пароль не знает: в нём только уже открытые сессии, и только в памяти. После перезапуска Redis все сессии пропадают, люди входят заново.

Файл учёток: `/opt/docusaurus-docs/authelia/users_database.yml`.  
В git его нет. В коммит его не класть.

Кто может войти: любой пользователь из этого файла, у которого пароль подошёл. Отдельных прав «только этот продукт» у логина нет. Группа `admins` в файле записана, но сайт по ней доступ не режет.

**Сессия:**

После входа Authelia отдаёт браузеру cookie `authelia_session`. По ней nginx понимает, что этот человек уже входил. Cookie привязана к имени `95.183.13.164.sslip.io`. Браузер не сохраняет такую cookie на голый IP, поэтому Admin открывают как `https://95.183.13.164.sslip.io:3002/`, а не как `https://95.183.13.164:3002/`.

Сессия живёт не бесконечно:

- 4 часа никто не открывал сайт — сессия кончается. Следующий заход снова спросит пароль.
- Сайтом пользуются без перерыва — всё равно через 12 часов после входа сессия кончается, и пароль спрашивают снова.

Поэтому Admin открывают по имени `sslip.io`, а не по голому IP. На голый IP такая cookie не сажается.

Кнопка **Log out** в шапке сбрасывает сессию. Тот же адрес можно открыть самому:  
`https://auth.95.183.13.164.sslip.io:3002/logout`

Письмо «забыл пароль» никуда не уходит. Уведомления Authelia пишет в файл внутри контейнера. Пароль меняют на сервере.

### Первый пользователь

Если `users_database.yml` ещё нет, его создаёт скрипт из A4:

```bash
cd /opt/docusaurus-docs
bash scripts/authelia-bootstrap.sh 'ВашПароль'
```

Он же дописывает в `.env` три ключа Authelia, если их ещё не было. После этого нужен деплой, как написано в выводе скрипта.

### Сменить пароль единственного admin

Тот же скрипт **перезаписывает весь** `users_database.yml` и оставляет в нём одного пользователя `admin`.

Так делать можно, пока в файле только он. Если там уже есть другие люди, скрипт их сотрёт.

Старые сессии в Redis от смены хеша сами не умирают. Человек с ещё живой cookie остаётся внутри, пока не выйдет или пока не кончатся 4 / 12 часов. Выкинуть всех сейчас:

```bash
cd /opt/docusaurus-docs
docker compose restart redis
```

Новый вход уже проверяется по новому хешу. Файл смонтирован в контейнер, Authelia следит за ним (`watch: true`), отдельная пересборка сайта для смены пароля не нужна.

### Добавить ещё одного человека

Bootstrap для этого не запускать.

На сервере, в том же каталоге:

```bash
cd /opt/docusaurus-docs
docker run --rm authelia/authelia:4.38.19 \
  authelia crypto hash generate argon2 --password 'ПарольЭтогоЧеловека'
```

В вывод попадет строка `Digest:`. В файл учёток копируют только сам хеш, без слова `Digest:`. Хеш в кавычках: в нём есть `$`.

В `authelia/users_database.yml` добавляют блок рядом с `admin`, файл целиком не заменяют:

```yaml
  ivan:
    disabled: false
    displayname: 'Ivan'
    password: '$argon2id$…хеш…'
    email: ivan@taigaiot.com
    groups:
      - admins
```

Имя слева (`ivan`) — это логин. Пароль в чат и в git не копировать.

Отключить человека, не стирая блок: `disabled: true`.

---

# Часть B. GitLab CI/CD

## B1. Runner `docs_server_instance`

**Group / Project → Settings → CI/CD → Runners**

Runner стоит на `95.183.13.164` и выполняет команды прямо на сервере. Поэтому `deploy_docs` видит `/opt/docusaurus-docs` и Docker этой машины.

В jobs указан тег:

```yaml
tags:
  - docs_server_instance
```

Если раннер выключен или проекту его не выдали, job висит в `Pending`.

![](/img/admin-runner-docs-server.png)

## B2. Переменные CI/CD

Хранить только в **Settings → CI/CD → Variables**. Не в `.gitlab-ci.yml`, не в скриптах, не в `.gitmodules`, не в коммите.

Группа / проект:

| Переменная | Зачем | Где нужна |
| --- | --- | --- |
| `DOCS_TRIGGER_TOKEN` | product CI запускает pipeline проекта 177 | продукт и группа |
| `GITLAB_PULL_TOKEN` | `read_repository`: деплой читает umbrella и все продукты | umbrella |
| `DOCS_PUSH_TOKEN` | `write_repository` на umbrella: записать новый SHA продукта | umbrella |
| `DOCS_HISTORY_TOKEN` | `read_api`: кнопка истории. Дублируется в `.env` на сервере | umbrella |

По желанию:

| Переменная                | Зачем                                                                                                                                                                                                                                                         |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DOCS_PRODUCT_PUSH_TOKEN` | push обратно в продукт, когда CI переписал картинки Obsidian. <br>Если пусто, CI пробует job token. Ошибка push **не роняет** pipeline: картинки всё равно перепишутся при сборке Admin |
| `DOCS_SITE_DIR` | каталог Admin на сервере, по умолчанию `/opt/docusaurus-docs` |
| `DOCS_USER_SITE_DIR` | каталог User на сервере, по умолчанию `/opt/docusaurus-docs-user` |

`DOCS_USER_PUSH_TOKEN` для текущего выпуска **не нужен**: публичный сайт собирается на сервере из тега umbrella, во второй Git-репозиторий ничего не пушится.

### Откуда берётся `DOCS_TRIGGER_TOKEN`

**Проект 177 → Settings → CI/CD → Pipeline trigger tokens.**  
Создать токен и положить значение в групповую переменную `DOCS_TRIGGER_TOKEN`.

### Права остальных токенов

- `GITLAB_PULL_TOKEN` — `read_repository` на umbrella и на каждый продукт, который деплой подтягивает.
- `DOCS_PUSH_TOKEN` — `write_repository` на `taiga_docs/taigadocs/taigadocs-internal`.
- `DOCS_HISTORY_TOKEN` — `read_api` на umbrella и на репозитории продуктов (история статей).

---

# Часть C. CI продукта

## C1. Один шаблон на все продукты

В корне продукта только:

```yaml
include:
  - project: 'taiga_docs/taigadocs/taigadocs-internal'
    ref: main
    file: '/ci/product-trigger.gitlab-ci.yml'
```

Логика меняется в `ci/product-trigger.gitlab-ci.yml` umbrella. Десятки копий в продуктах не правят.

## C2. Две задачи, не сборка сайта

Product CI **не собирает Docusaurus и не деплоит**.

Стадии:

```text
convert_obsidian_embeds
  ↓
trigger_docs_rebuild
```

`convert_obsidian_embeds` на раннере:

1. Берёт скрипт из `/opt/docusaurus-docs` (если каталог пуст — скачивает его из проекта 177).
2. `convert-obsidian-embeds.sh`  
   `![[photo.png]]` → `![](img/photo.png)`  
   `![[scheme.svg]]` → файл в `diag/`  
   `![[manual.pdf]]` → окно просмотра и ссылка на `/files/docs/...`  
   Ссылки вида `[[другая-статья]]` скрипт **не** трогает. Примеры в блоках кода гайда пропускает.
3. Если что-то изменилось — коммит и push в `main` продукта с `ci.skip`, чтобы не запустить второй такой же pipeline.
4. В umbrella уходит SHA **после** этих коммитов, если push удался. Если push не удался, уходит SHA коммита техписа, а переписывание картинок повторит `deploy_docs`.

Номер `doc_rev` product CI **не** пишет. Его считает сборка Admin (и User) в копии дерева `.docs-build` при `npm run build` / Docker-сборке. В репозиторий продукта этот номер из сборки обратно не коммитится.

Дальше `trigger_docs_rebuild` вызывает Trigger API проекта 177 и передаёт:

| Переменная | Значение |
| --- | --- |
| `TRIGGERED_BY` | путь проекта, например `taiga_docs/taigabadgelw/docs` |
| `SUBMODULE_SHA` | SHA, который надо зафиксировать |

Путь submodule в umbrella будет `docs/<TRIGGERED_BY>`.

---

# Часть D. Pipeline umbrella — проект 177

Стадии: `sync` → `deploy` → `publish_user`.  
`publish_user` в обычном push **нет**. Он есть только у тега.

## D1. `sync_submodule`

Скрипт `scripts/ci-sync-submodule.sh`. Запускается, только если pipeline пришёл от trigger и передан `SUBMODULE_SHA`.

```text
TRIGGERED_BY
  ↓
путь docs/<TRIGGERED_BY>
  ↓  (если нет — add; если был старый путь — relocate)
gitlink = SUBMODULE_SHA
  ↓
commit + push в main umbrella с ci.skip
```

Сообщение коммита похоже на `chore(docs): bump … to 3c69f1f`.

Push с `ci.skip` **не** запускает второй pipeline. Иначе получилась бы петля trigger → sync → push → trigger. Сборка идёт в том же pipeline, который запустил продукт.

Нормальная картина:

```text
Pipeline 177 (его создал продукт)
├── sync_submodule   Passed
└── deploy_docs      Passed

Отдельный pipeline у коммита bump
└── Skipped          ← так и должно быть
```

`Skipped` у bump-коммита — не ошибка, если исходный pipeline дошёл до `deploy_docs`.

Прямой push в `main` umbrella (правка этого гайда, CI, тема) **не** вызывает `sync_submodule`. Сразу начинается `deploy_docs`.

## D2. `deploy_docs`

Обновляет клон на сервере и собирает Admin.

```text
git fetch
git reset --hard origin/main
git submodule update --init --recursive --force
git clean -ffd -- docs/ (очищаем хвосты на серве(если какие-то репо были del))
convert-obsidian-embeds.sh на всё дерево docs/
sanitize-svg.sh
check-doc-images.sh
scripts/ci-deploy-compose.sh
  → prepare-docs-build (копия + doc_rev)
  → copy-raw-docs (/raw/docs/…)
  → generate-files-list («Файлы» + _files-all.zip)
  → check-doc-links
  → docusaurus build
```

`git submodule update` берёт SHA из gitlink. Флага `--remote` нет: сайт не подтягивает «просто последний» коммит продукта, только тот, который записал sync.

`check-doc-images.sh` останавливает публикацию, если картинка не найдена или в статье осталась живая вставка `![[…]]` вне блока кода. Примеры в гайде техписа на это не падают: они в обратных кавычках или в блоке кода.

Номер `doc_rev` появляется на сайте при сборке. Техпису он нужен для поиска: на сайте его можно скопировать, поиск ищет и по нему.

## D3. Почему SHA в bump может быть не тем, что пушил техпис

У продукта и umbrella разные коммиты.

```text
Коммит техписа в продукте:     aaaaaaa
Коммит CI (картинки Obsidian):  bbbbbbb   ← его часто и фиксирует umbrella
Коммит umbrella:               ccccccc   chore(docs): bump … to bbbbbbb
```

Если push из product CI прошёл, в bump будет `bbbbbbb`. Искать «пропавший» коммит техписа нужно в истории продукта: он родитель коммита CI.

---

# Часть E. Что видит читатель на Admin

После входа у статьи есть:

- поиск над деревом слева (в шапке строка поиска спрятана);
- скачивание `.md` с адреса `/raw/docs/...`;
- **Другое** → Export and download to PDF (печать браузера);
- история правок через `/api/history/` (контейнер history-api и `DOCS_HISTORY_TOKEN`);
- блок `doc_rev` и кнопка **Копировать**;
- страница **Файлы** в папках, где лежат «осиротевшие» вложения;
- **Log out**.

История продукта ходит в репозиторий продукта. История статьи самого umbrella — в проект 177, путь с пометкой `_umbrella`.

### Страница «Файлы» и архив

Если в папке со статьями лежат вложения (pdf, docx, xlsx, bin, zip, hex, txt, 7z, rar), на которые **ни одна** статья этой папки не ссылается, сборка считает их orphan — «осиротевшими».

Для такой папки сборка делает:

1. страницу `_files.md` в сайдбаре с названием **Файлы** — таблица ссылок на скачивание;
2. архив `static/files/docs/<путь>/_files-all.zip` — кнопка «Скачать всё архивом».

Файлы, которые уже вставлены в статью (`![[manual.pdf]]` и т.п.), в «Файлы» и в zip не попадают: они уже привязаны к тексту. Страница и zip живут только в сборке сайта, в репозиторий продукта их не коммитят.

Порядок в `npm run build`: сначала `copy-raw-docs` копирует вложения в `static/files/docs`, потом `generate-files-list-sidebar` пишет `_files.md` и zip. Иначе очистка `static/files` сотрёт только что собранный архив.

## E2. Две версии документации

Слово «версия» здесь про две разные вещи.

**Номер статьи,** `doc_rev`**.**  
Короткий код из 7 символов. Его считает сборка сайта, не техпис и не админ. Скрипт смотрит отпечаток текста и локальных картинок. Текст не менялся — номер тот же. Текст или картинка изменились — пишется новый номер. Дата в `doc_rev_at` и тот же код в `keywords` нужны, чтобы поиск находил статью по номеру. На сайт для читателя снаружи этот номер сам по себе ничего не выпускает.

**Снимок публичного сайта, тег** `docs-*`**.**  
Подпись на одном коммите umbrella. User показывает последний такой тег, а не сегодняшний `main`, **то есть версию репозитория в момент создания тега**. Между двумя тегами на Admin номера `doc_rev` могут смениться много раз. Наружу уезжает тот текст и те номера, которые были в коммите тега.

Откатить изменения на публичном сайте — повторить `publish_user` с тем тегом, до версии которого нужно откатиться. Admin от этого на старый коммит не встаёт.

---

# Часть F. Полный рабочий процесс

## F1. Техпис изменил существующий продукт

```text
Obsidian → commit → push main продукта
        ↓
convert_obsidian_embeds
        ↓
trigger_docs_rebuild
        ↓
Pipeline 177: sync_submodule → deploy_docs
        ↓
Admin обновился
User не изменился
```

Пока идёт `deploy_docs`, второй деплой того же сайта ждёт: у job стоит `resource_group: docs-production`. Поэтому «подождите пару минут» иногда превращается в дольше.

## F2. Выпуск на User

Admin показывает последний `main`. User показывает последний тег `docs-*` на umbrella.

Тег — подпись на одном конкретном коммите umbrella. В этом коммите уже записаны SHA всех продуктов. Отдельные теги в репозиториях продуктов не нужны.

Один раз в проекте `taiga_docs/taigadocs/taigadocs-internal`:

```text
Settings → Repository → Protected tags
```

Шаблон: `docs-*`. Создавать может Maintainer. Техпис с правом push в продукт этот тег создать не может. Из репозитория эта защита не включается.

Как выпустить:

1. Дождаться зелёного pipeline Admin.
2. Deploy → Releases → New release.
3. Имя тега: `docs-ГГГГ-ММ-ДД`. Источник: `main`.
4. Pipeline этого тега запускает только `publish_user`. `deploy_docs` на теге не бежит, Admin не откатывается.
5. Job `scripts/ci-deploy-public.sh` берёт **этот тег** в отдельный каталог, не живую папку `/opt/docusaurus-docs`. Подтягивает продукты тех SHA, которые в теге, копирует allowlist в шаблон User и собирает `/opt/docusaurus-docs-user`. Во второй Git ничего не пушится.
6. На `:3003` поднимается публичный сайт.

Что смотреть в allowlist — файл `allowlist-public.yml`. Если абзац ниже и файл разойдутся, верьте файлу. Сейчас в `include` есть деревья `docs/products/taigabadgelw/**`, `docs/products/toesnode/**`, зеркала `docs/taiga_docs/taigabadgelw/**`, `docs/taiga_docs/toesnode/**`, плюс `docs/intro.md` и `_category_.json`.

Гайды техписа и админа, `.env`, `.obsidian` на User не копируются. Чтобы показать снаружи другой продукт, его путь добавляют в `include` этого файла и выпускают новый тег. Push техписа сам файл allowlist не расширяет.

Откат User: в pipeline старого тега повторить job `publish_user`. Admin остаётся на `main`.

Проверка, что контур живой:

- push в продукт меняет `:3002` и не трогает `:3003`;
- New release создаёт `publish_user` и обновляет `:3003`;
- следующий push снова двигает только Admin.

---

## F3. Статья на публичном сайте

Push техписа обновляет только Admin. На `http://95.183.13.164:3003/` статья появляется после тега `docs-*`, и только если её путь разрешён в `allowlist-public.yml`.

Как поставить тег — в F2. Здесь — решить, попадёт ли эта статья в копирование.

### Статья уже внутри разрешённого продукта

Новая страница внутри уже включённого дерева (`**`) в allowlist не дописывается. Шаблон забирает всё дерево продукта, которое записано в теге.

Порядок:

1. Дождаться, пока статья есть на Admin и pipeline umbrella зелёный. В коммите umbrella уже должен быть SHA продукта с этой статьёй.
    
2. Поставить тег `docs-*` на этот `main`, как в F2.
       
3. Создать релиз с этим тегом.

Тег раньше, чем `sync_submodule` записал новый SHA, скопирует старое дерево продукта. Статьи на User не будет, хотя на Admin она уже открывается.

### Статья или продукт в allowlist ещё не входят

Сначала правка umbrella, потом тег. Правка только на сервере не живёт: следующий `deploy_docs` делает `reset --hard`.

В `include` добавляют строку:

| **Что показать**                 | **Строка**                             |
| -------------------------------- | -------------------------------------- |
| Весь продукт                     | `docs/<gitlab-path>/**`                |
| Один файл                        | `docs/<gitlab-path>/папка/статья.md`   |
| Файлы одной папки, без вложенных | `docs/<gitlab-path>/папка/*.md`        |

Одна строка на `.md` копирует только этот файл. Картинки рядом и `_category_.json` этой папки сами не приедут, меню продукта на User соберётся дырявым. Для продукта проще одна строка на всё дерево.

`exclude` сильнее `include`. Даже внутри `**` на User не попадают:

- `**/internal/**`
    
- `**/.obsidian/**`
    
- гайды `docs/guide to *.md`
    
- `.env`, `CHAT_CONTEXT.md`, `.gitlab-ci.yml`
    

Дальше:

1. Commit и push umbrella.
    
2. Дождаться, что этот коммит на `main`. Если вместе с ним должен уехать и свежий текст продукта — зелёный `deploy_docs` с нужным SHA, как в пункте выше.
    
3. Тег `docs-*` на этот `main` и релиз.
    

`publish_user` берёт дерево **тега**, не живую папку `/opt/docusaurus-docs`, оставляет только allowlist и собирает User на сервере. Проверка: страница открывается на `:3003`. Гайда админа и гайда техписа там быть не должно.

---

# Часть G. Добавление нового продукта

Добавление нового продукта выполняет **администратор**.

Техническому писателю после настройки нужно только получить доступ к product-репозиторию и работать через Obsidian.

## G1. Чеклист

| #   | Действие                                         | Где              |
| --- | ------------------------------------------------ | ---------------- |
| 1   | Создать product-репозиторий                      | GitLab           |
| 2   | Создать `.gitlab-ci.yml` с центральным `include` | Product          |
| 3   | Проверить доступ runner `docs_server_instance`   | GitLab           |
| 4   | Проверить Group CI/CD Variables                  | GitLab           |
| 5   | Сделать тестовый push в product                  | Product          |
| 6   | Дождаться sync + deploy в проекте 177            | GitLab / браузер |
| 7   | При необходимости добавить путь в `allowlist-public.yml` | Umbrella   |

Руками добавлять submodule в umbrella обычно не нужно: `sync_submodule` сам создаст путь `docs/<CI_PROJECT_PATH>` и промежуточные `_category_.json`.

---

## G2. Создать product-репозиторий

В нужной группе GitLab создать новый проект. Пример:

```text
taiga_docs/новый-продукт/docs
```

Основная ветка:

```text
main
```

После первого успешного trigger submodule окажется здесь:

```text
docs/taiga_docs/новый-продукт/docs
```

---

## G3. Настроить `.gitlab-ci.yml`

В корне product-репозитория создайте файл:

```text
.gitlab-ci.yml
```

внутри должно быть:

```yaml
include:
  - project: 'taiga_docs/taigadocs/taigadocs-internal'
    ref: main
    file: '/ci/product-trigger.gitlab-ci.yml'
```

---

## G4. Проверить Runner

У нового product должен быть доступен runner с тегом:

```text
docs_server_instance
```

Если runner групповой, достаточно убедиться, что проект имеет право его использовать.

![](/img/admin-runner-docs-server.png)

---

## G5. Проверить CI/CD Variables

Для product должен быть доступен:

```text
DOCS_TRIGGER_TOKEN
```

Найти его можно в настройках CI/CD umbrella (проект 177) в разделе **Pipeline trigger tokens**. Значение кладут в CI/CD Variables группы или продукта.

Остальные токены (`GITLAB_PULL_TOKEN`, `DOCS_PUSH_TOKEN`) используются в TaigaDocs CI/CD и не требуют добавления в каждый product отдельно, если они уже настроены на уровне группы.

---

# Часть H. Подключение продукта как submodule

Обычный путь — push в продукт с готовым `include`. Sync сам:

1. вычислит `docs/<CI_PROJECT_PATH>`;
2. создаст родительские папки и `_category_.json`, если их ещё нет;
3. добавит submodule или перенесёт его со старого пути;
4. запишет SHA и запушит bump в umbrella с `ci.skip`.

Если всё же нужно добавить submodule вручную с ПК администратора:

```powershell
cd C:\taiga-docs
git pull origin main
```

Добавление (путь должен совпасть с будущим path-mirror):

```bash
git submodule add -b main <URL_репозитория> docs/<gitlab-group/.../project>
```

Пример:

```bash
git submodule add -b main https://gitlab.taigaiot.com/taiga_docs/новый-продукт/docs.git docs/taiga_docs/новый-продукт/docs
```

После этого проверить:

```bash
git status
git diff -- .gitmodules
```

В `.gitmodules` должна появиться запись вида:

```ini
[submodule "docs/taiga_docs/новый-продукт/docs"]
    path = docs/taiga_docs/новый-продукт/docs
    url = https://gitlab.taigaiot.com/taiga_docs/новый-продукт/docs.git
    branch = main
```

Затем:

```bash
git add .gitmodules docs/taiga_docs/новый-продукт/docs
git commit -m "docs: add новый-продукт docs submodule"
git push origin main
```

:::danger
Путь submodule должен быть стабильным и совпадать с `docs/<CI_PROJECT_PATH>`.

Не переименовывать путь в папку с пробелами или «красивым» отображаемым названием.

Отображаемое название продукта задаётся в Docusaurus/Markdown, а путь submodule остаётся техническим.
:::

#### Важно:

У продукта и umbrella разные Git-коммиты.

Например:

```text
Product:
3c69f1f

Umbrella:
d1e4c94
```

`3c69f1f` — commit в репозитории продукта.

`d1e4c94` — commit umbrella, который зафиксировал:

```text
docs/taiga_docs/taigabadgelw/docs → 3c69f1f
```

Поэтому при диагностике всегда нужно понимать, какой SHA проверяется.

---

# Часть I. Проверка, если «на сайте старое»

## I1. Продукт

**Product → Build → Pipelines.** Должны пройти `convert_obsidian_embeds` и `trigger_docs_rebuild`.

Если pipeline нет или он `Pending`:

- push был не в `main`;
- нет `.gitlab-ci.yml` с include;
- нет `DOCS_TRIGGER_TOKEN`;
- раннер `docs_server_instance` не выдан проекту.

Предупреждение `push failed` внутри convert — ещё не провал всего pipeline. Смотрите, дошёл ли trigger. Картинки тогда перепишутся на шаге Admin.

## I2. Umbrella

В проекте 177 pipeline от Trigger API:

```text
sync_submodule   Passed
deploy_docs      Passed
```

`publish_user` в этом списке быть не должно.

В истории umbrella ищите коммит вида `chore(docs): bump … to 3c69f1f`: имя/путь продукта и короткий SHA.

На ПК админа, в `C:/taiga-docs`, после `git pull`:

```bash
git submodule status
```

Строка продукта должна показывать тот SHA, который ушёл в trigger. Помните часть D3: это может быть коммит CI, а не коммит техписа.

## I3. Сервер

```bash
cd /opt/docusaurus-docs
git rev-parse --short HEAD
docker compose ps
```

HEAD должен совпасть с `main`, на котором закончился `deploy_docs`. Контейнер `docs` — Up, Authelia — healthy.

Открывать проверку нужно как **https://95.183.13.164.sslip.io:3002/**, не как `http://IP:3002`.

---

# Часть J. Типичные ошибки

| Симптом | Что проверить |
| --- | --- |
| Product pipeline `Pending` | Runner `docs_server_instance` не assigned |
| `DOCS_TRIGGER_TOKEN is not set` | Групповая переменная или trigger token проекта 177 |
| Продукт зелёный, pipeline 177 не появился | URL trigger, токен, project id 177 |
| `sync_submodule` не находит / не добавляет продукт | `TRIGGERED_BY` = `CI_PROJECT_PATH`, права `GITLAB_PULL_TOKEN` / `DOCS_PUSH_TOKEN` |
| `deploy_docs` падает на git | Каталог `/opt/docusaurus-docs`, `GITLAB_PULL_TOKEN` |
| `deploy_docs` падает на submodule | SHA существует, токену хватает `read_repository` на этот продукт |
| `DOCS_HISTORY_TOKEN is empty` | Переменная CI и/или `.env`. Пустой `export DOCS_HISTORY_TOKEN=` в shell перекрывает `.env` |
| `container name already in use` | Кто-то удалил контейнеры до конца сборки. Повторять через `scripts/ci-deploy-compose.sh` |
| Картинка `![[…]]` уронила проверку | Файл не закоммичен или это не картинка/PDF. Ссылки `[[статья]]` так не чинятся |
| PDF на сайте пустой | В iframe должен быть абсолютный `/files/docs/...`. Относительный `files/...` Docusaurus в iframe не открывает. Это чинит convert, если известен путь продукта |
| Нет страницы «Файлы» / нет zip | В папке нет orphan-вложений, или сборка не дошла до `generate-files-list-sidebar` |
| Admin не обновился | `sync_submodule`, `deploy_docs`, не открыт ли старый HTTP-адрес |
| User не обновился после push техписа | Так и задумано. Нужен тег `docs-*` |
| Тег есть, `publish_user` нет | Тег указывает на коммит, в котором этого правила в `.gitlab-ci.yml` ещё не было |
| User собрался, но продукта нет | Пути нет в `allowlist-public.yml` |
| Pipeline bump-коммита `Skipped` | Нормально, если trigger-pipeline выполнил `deploy_docs` |
| Диск 100% | Кэш BuildKit. Лимит 2 ГБ, не `prune -af` как постоянное решение |
| Сайт открывается без логина с голого IP по старому порту | Это не Admin. Проверьте, что смотрите sslip.io:3002 |

---

# Часть K. Файлы, которые имеет смысл помнить

| Файл                                 | Зачем                                           |
| ------------------------------------ | ----------------------------------------------- |
| `.gitmodules`                        | Какой репозиторий в какой папке                 |
| `.gitlab-ci.yml`                     | Pipeline umbrella                               |
| `ci/product-trigger.gitlab-ci.yml`   | Общий CI продуктов                              |
| `allowlist-public.yml`               | Что копируется на User                          |
| `scripts/ci-sync-submodule.sh`       | Запись нового SHA / path-mirror                 |
| `scripts/ci-deploy-compose.sh`       | Сборка и запуск Admin без гонки имён            |
| `scripts/ci-deploy-public.sh`        | Тег `docs-*` → каталог User на сервере          |
| `scripts/convert-obsidian-embeds.sh` | Форматирование картинок, SVG и PDF из Obsidian  |
| `scripts/bump-doc-revs.cjs`          | Номера `doc_rev` при сборке                     |
| `scripts/prepare-docs-build.cjs`     | Копия `docs` → `.docs-build` + bump             |
| `scripts/generate-files-list-sidebar.cjs` | Страница «Файлы» и `_files-all.zip`        |
| `scripts/check-doc-images.sh`        | Стоп публикации, если картинка битая            |
| `scripts/backup-admin-stack.sh`      | Снимок перед сменой инфры                       |
| `authelia/configuration.yml`         | Домены логина                                   |
| `users_database.yml`                 | Логины и пароли (только на сервере)             |
| `docker-compose.yml`                 | Admin                                           |
| `docker-compose.lockdown.yml`        | 3002 только на localhost                        |
| `templates/taiga-docs-user/`         | Шаблон публичного сайта                         |

---

# Часть L. Правила

1. Токены и `.env` не коммитить. `users_database.yml` тоже.
2. SHA submodule руками не переписывать, если не хотите зафиксировать чужую версию мимо CI.
3. На проде не использовать `git submodule update --remote`.
4. Новый продукт не добавлять в `case` sync-скрипта — пути считает path-mirror.
5. Product CI не собирает сайт.
6. Менять поведение всех продуктов в одном файле: `ci/product-trigger.gitlab-ci.yml`.
7. Правки umbrella — через Git, не правкой файлов только на сервере. Следующий `deploy_docs` сделает `reset --hard` и затрёт ручные правки в `/opt/docusaurus-docs`.
8. После правки CI смотреть реальный pipeline, не только зелёный diff.
9. Не путать SHA техписа, SHA коммита CI в продукте и SHA umbrella.
10. Техпису не нужны umbrella, submodule и сервер.
11. User не обновляется push-ем техписа.
12. Перед сменой портов, TLS или Authelia — `scripts/backup-admin-stack.sh`.
13. Контейнеры Admin не удалять вручную, пока идёт сборка образа.
14. Публичный allowlist править в `allowlist-public.yml`, не в живой папке на сервере.

---

# Часть M. Схема

```text
PRODUCT main
  │ push техписа
  ▼
convert_obsidian_embeds
  │ картинки / PDF, push с ci.skip если смог
  ▼
trigger_docs_rebuild
  │ TRIGGERED_BY + SUBMODULE_SHA
  ▼
UMBRELLA 177
  ├── sync_submodule
  │     docs/<TRIGGERED_BY> = SHA, push bump с ci.skip
  └── deploy_docs
        reset + submodule update
        проверка картинок
        ci-deploy-compose.sh
          prepare-docs-build (doc_rev)
          «Файлы» + _files-all.zip
              ▼
        Admin  https://95.183.13.164.sslip.io:3002/

Когда нужно обновить читателя снаружи
  │ тег docs-* на текущем main
  ▼
publish_user
  │ allowlist этого тега → /opt/docusaurus-docs-user
  ▼
User  http://95.183.13.164:3003/
```

Коротко:

> Продукт хранит текст.  
> Его CI приводит вставки Obsidian к виду сайта и сообщает umbrella нужный SHA.  
> Umbrella запоминает этот SHA по пути `docs/<путь проекта в GitLab>`.  
> `deploy_docs` собирает именно его на внутренний сайт с логином.  
> Публичный сайт получает только то, что перечислено в `allowlist-public.yml`, и только когда на umbrella поставлен тег `docs-*`.

doc_rev: l1kupec
