From 840554c223d158eed89836660b6764a27243164b Mon Sep 17 00:00:00 2001 From: Zuev Date: Wed, 5 Aug 2026 15:37:34 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=86=D0=B5=D0=BF=D1=82=20AI?= =?UTF-8?q?=20=D0=B0=D1=81=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AI_ASSISTANT_IMPLEMENTATION_PLAN.md | 1578 +++++++++++++++++++++++++++ DEVOPS.md | 292 ----- STARTUP_GUIDE.md | 162 --- 3 files changed, 1578 insertions(+), 454 deletions(-) create mode 100644 AI_ASSISTANT_IMPLEMENTATION_PLAN.md delete mode 100644 DEVOPS.md delete mode 100644 STARTUP_GUIDE.md diff --git a/AI_ASSISTANT_IMPLEMENTATION_PLAN.md b/AI_ASSISTANT_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..9cd9e86 --- /dev/null +++ b/AI_ASSISTANT_IMPLEMENTATION_PLAN.md @@ -0,0 +1,1578 @@ +# План реализации отдельного AI-ассистента для Magistr + +> Статус: исходная техническая спецификация для создания отдельного проекта +> +> Версия документа: 1.0 +> +> Дата: 5 августа 2026 года +> +> Основной интегрируемый продукт: Magistr +> +> Рабочее название нового репозитория: `magistr-assistant` + +## 1. Назначение документа + +Документ описывает реализацию отдельного коммерческого микросервиса с персонажем-проводником для Magistr. Ассистент помогает пользователям находить страницы и элементы интерфейса, объясняет последовательность действий, подсвечивает нужные элементы и принимает запросы текстом или голосом. + +Документ должен использоваться как: + +- исходная архитектурная спецификация нового репозитория; +- основание для декомпозиции задач в Gitea; +- контракт между новым сервисом и Magistr; +- перечень обязательных проверок перед пилотом; +- фиксация продуктовых ограничений первого релиза; +- точка продолжения для последующего режима выполнения действий после подтверждения пользователя. + +Этот документ не является разрешением на создание нового репозитория, изменение production-инфраструктуры или подключение платных AI API. Такие действия выполняются отдельными этапами после явного начала реализации. + +## 2. Зафиксированные продуктовые решения + +| Вопрос | Решение | +|---|---| +| Пользователи первого релиза | `ADMIN`, `EDUCATION_OFFICE`, `DEPARTMENT` | +| Возможности первого релиза | Подсказки, переходы по разрешённым страницам и подсветка элементов | +| Автоматическое выполнение действий | Не входит в первый релиз; возможно позже, только после явного подтверждения | +| Языки | Русский и английский | +| Персонаж | Один для всех университетов | +| Образ | Животное или абстрактный проводник; окончательный образ выбирается позже | +| Текстовый AI | Внешний российский сервис по API через заменяемый адаптер | +| Голос | Push-to-talk: запись только пока пользователь удерживает кнопку | +| Управление подпиской | Только оператор платформы | +| Оплата | Вручную по договору, без платёжной системы | +| Репозиторий | Отдельный репозиторий и независимый релизный цикл | +| Данные диалогов | Полный текст и аудио не сохраняются | +| Аналитика | Только обезличенная статистика и явная оценка ответа | +| Retention пользовательской аналитики | Автоматическое удаление данных старше 30 дней | + +## 3. Цель первого релиза + +Пользователь должен иметь возможность открыть персонажа, задать вопрос текстом или голосом и получить короткую инструкцию. Если пользователь имеет право на соответствующий раздел, интерфейс Magistr должен перейти на нужную страницу и подсветить следующий элемент. Если права отсутствуют, ассистент должен прямо сообщить об ограничении и указать, к какой роли следует обратиться. + +Пример ожидаемого поведения: + +1. Пользователь спрашивает: «Где менять время пар в субботу?». +2. Ассистент проверяет роль и доступные возможности. +3. Для `ADMIN` или `EDUCATION_OFFICE` открывает настройки временных слотов. +4. Подсвечивает выбор области действия и объясняет, что нужна субботняя сетка. +5. Для `DEPARTMENT` не пытается открыть запрещённую страницу и сообщает, что изменение выполняет администратор или учебный отдел. + +## 4. Что не входит в первый релиз + +- автоматическое нажатие кнопок; +- заполнение или отправка форм; +- создание, изменение и удаление данных Magistr; +- доступ ассистента к расписаниям, пользователям и другим бизнес-данным напрямую через API Magistr; +- чтение всего DOM и отправка содержимого страницы AI-провайдеру; +- долговременная память о пользователе; +- сохранение истории диалога на сервере; +- фоновой постоянно включённый микрофон; +- распознавание пользователя по голосу; +- 3D-персонаж, VRM, Live2D или сложный игровой движок; +- пользовательская настройка персонажа университетом; +- личный кабинет оплаты; +- онлайн-эквайринг, автопродление и возвраты; +- внешняя веб-навигация и поиск в интернете; +- поддержка ролей `SCHEDULE_VIEWER`, `TEACHER` и `STUDENT` в первом пилоте; +- использование существующих tenant-БД Magistr для данных подписки и аналитики ассистента. + +## 5. Подтверждённые ограничения текущего Magistr + +### 5.1. Frontend + +Основная административная панель — Vanilla JavaScript SPA. Вкладка выбирается через hash и `data-tab`, после чего HTML загружается динамически в `#app-content`. Инициализация конкретного экрана завершается только после асинхронного импорта его JS-модуля. + +Страница настроек — отдельный SPA со своим набором маршрутов. Переход из основной панели в настройки приводит к загрузке нового документа. + +Кабинет кафедры находится внутри основной административной панели. Для роли `DEPARTMENT` целевой режим просмотра расписаний принудительно ограничен собственной кафедрой. + +Следствия для ассистента: + +- подсветка возможна только после завершения загрузки и инициализации представления; +- при переходе между основной панелью и настройками необходимо безопасно переносить незавершённое действие; +- нельзя рассчитывать на случайные CSS-классы и текст кнопки как на стабильный контракт; +- динамические элементы требуют resolver-функций, а не статического селектора; +- ассистент обязан учитывать роль до перехода, а клиент обязан повторно проверить действие. + +### 5.2. Ролевая модель + +| Возможность | ADMIN | EDUCATION_OFFICE | DEPARTMENT | +|---|---:|---:|---:| +| Открыть календарный учебный график | Да | Да | Нет | +| Изменить код дня на `*` | Да | Да | Нет | +| Открыть настройки временных слотов | Да | Да | Нет | +| Изменить субботнюю сетку времени | Да | Да | Нет | +| Просматривать расписания | Да | Да | Только своя кафедра | +| Искать конкретную группу прямым фильтром | Да | Да | Нет, режим зафиксирован на кафедре | +| Создать точечный перенос занятия | Да | Да | Нет | + +Ролевая недоступность является ожидаемым результатом workflow. Она не должна оформляться как техническая ошибка или компенсироваться попыткой открыть скрытый раздел. + +### 5.3. Мультитенантность + +Tenant Magistr определяется по домену запроса. Access JWT связан с tenant и не принимается на другом домене. Новый сервис не должен получать основной симметричный `JWT_SECRET` Magistr. + +Подписки ассистента хранятся централизованно в собственной БД, где ключом является стабильный `tenantKey`, совпадающий с нормализованным tenant Magistr. Выключение подписки должно блокировать API на сервере, а не только скрывать виджет. + +### 5.4. Browser security + +Frontend Magistr использует строгую Content Security Policy: + +- скрипты и соединения только same-origin; +- inline-скрипты запрещены; +- произвольные inline-стили запрещены; +- `unsafe-eval` отсутствует; +- доступ к микрофону сейчас запрещён через `Permissions-Policy`. + +Следствия: + +- виджет распространяется как проверенный ESM-артефакт и включается в frontend-сборку Magistr; +- API публикуется через same-origin маршрут `/assistant/*`; +- подсветка реализуется CSS-классом на целевом элементе, а не динамическим inline-стилем; +- текст ответа вставляется через `textContent`, без исполнения HTML; +- для голоса разрешается только `microphone=(self)`; +- `media-src` и `worker-src` расширяются только при доказанной необходимости конкретной реализации; +- сбой ассистента не должен блокировать загрузку Magistr. + +## 6. Стартовые пользовательские сценарии + +### 6.1. Добавить выходной в четверг текущей недели + +Идентификатор намерения: `calendar.set_non_working_day`. + +Доступ: `ADMIN`, `EDUCATION_OFFICE`. + +Требуемый пользовательский путь: + +1. Открыть «Календарный учебный график». +2. Открыть внутреннюю вкладку «Сетки». +3. Выбрать нужный календарный график. +4. Нажать «Загрузить сетку». +5. Найти ячейку четверга нужной недели и курса. +6. Выбрать код `* — Нерабочий праздничный день`. +7. Нажать «Сохранить сетку». + +Ассистент не знает, какой учебный график и курс имеет в виду пользователь, поэтому не выбирает их самостоятельно. Он подсвечивает выбор графика и объясняет оставшиеся шаги. Если в будущем появится безопасный read-only контекст выбранного графика, workflow можно сделать точнее. + +Поведение для `DEPARTMENT`: + +- не выполнять переход; +- ответить, что календарный график изменяют администратор или учебный отдел; +- предложить передать им дату, курс и название группы/графика. + +### 6.2. Изменить время пар в субботу + +Идентификатор намерения: `time_slots.edit_saturday`. + +Доступ: `ADMIN`, `EDUCATION_OFFICE`. + +Требуемый пользовательский путь: + +1. Открыть «Настройки». +2. Перейти во вкладку «Временные слоты». +3. В поле «Область действия» выбрать системную субботнюю сетку. +4. Найти нужную пару в таблице. +5. Нажать «Изменить». +6. Указать начало и окончание. +7. Нажать «Сохранить слот». + +Поведение для `DEPARTMENT` аналогично предыдущему сценарию: объяснить отсутствие прав и указать ответственную роль. + +### 6.3. Посмотреть расписание группы ИБ-01б + +Идентификатор намерения: `schedule.view_group`. + +Вариант для `ADMIN` и `EDUCATION_OFFICE`: + +1. Открыть «Просмотр расписаний». +2. В поле «Что смотреть» выбрать «Группа». +3. В поле «Группа» найти `ИБ-01б`. +4. При необходимости выбрать семестр. +5. Нажать «Показать». + +Ассистент не должен читать или передавать AI-провайдеру полный список групп. Имя группы извлекается из пользовательской фразы и остаётся частью подсказки. На первом этапе ассистент подсвечивает поле группы, но не меняет его значение. + +Вариант для `DEPARTMENT`: + +1. Открыть «Просмотр расписаний». +2. Объяснить, что кафедра видит только расписания своей кафедры. +3. Подсветить кнопку «Показать». +4. После загрузки предложить выбрать группу среди найденных результатов. + +Если группа не относится к кафедре пользователя, ассистент должен сообщить об ограничении, а не предлагать обход. + +### 6.4. Перенести пару с одной даты на другую + +Идентификатор намерения: `schedule.move_lesson`. + +Доступ: `ADMIN`, `EDUCATION_OFFICE`. + +Требуемый пользовательский путь: + +1. Открыть «Просмотр расписаний». +2. Найти нужное расписание и период. +3. Нажать «Показать». +4. На карточке конкретной пары нажать «Изменить». +5. Выбрать доступную дату переноса. +6. Выбрать время, действующее для новой даты. +7. При необходимости уточнить преподавателя, аудиторию, формат и комментарий. +8. Проверить блок «Было / Стало». +9. Нажать «Сохранить». + +Ассистент не выбирает конкретное занятие сам и не сохраняет перенос. Для `DEPARTMENT` отвечает, что точечные изменения выполняют администратор или учебный отдел. + +## 7. Целевая архитектура + +```mermaid +flowchart LR + User[Пользователь Magistr] + Host[Frontend Magistr] + Loader[Assistant loader] + Widget[Assistant Web Component] + Bridge[Magistr UI bridge] + Caddy[Caddy / same-origin route] + API[magistr-assistant API] + Entitlement[Entitlement service] + Workflow[Workflow engine] + Provider[Российский AI provider] + DB[(Assistant PostgreSQL)] + OTel[OpenTelemetry Collector] + SigNoz[SigNoz] + + User --> Host + Host --> Loader + Loader --> Widget + Widget --> Bridge + Bridge --> Host + Widget -->|/assistant/v1/*| Caddy + Caddy --> API + API --> Entitlement + API --> Workflow + Workflow --> Provider + Entitlement --> DB + API --> DB + API --> OTel + OTel --> SigNoz +``` + +### 7.1. Границы ответственности + +#### Magistr + +- аутентифицирует пользователя; +- определяет tenant; +- выдаёт короткоживущий assistant-токен; +- сообщает роль и `departmentId` в ограниченном наборе claims; +- предоставляет host bridge для навигации и подсветки; +- содержит стабильные `data-assistant-target` на элементах; +- не передаёт новому сервису основной JWT-секрет; +- продолжает обеспечивать backend-авторизацию всех бизнес-операций. + +#### Новый сервис + +- проверяет assistant-токен; +- проверяет подписку tenant и разрешённую роль; +- распознаёт намерение; +- выбирает проверенный workflow; +- формирует двуязычную подсказку; +- возвращает только разрешённые семантические UI-действия; +- выполняет STT и TTS через провайдерские адаптеры; +- учитывает квоты; +- сохраняет только обезличенную статистику и оценку; +- удаляет пользовательскую аналитику старше 30 дней. + +#### AI-провайдер + +- получает только минимальный текст запроса, разрешённые workflow и локаль; +- не получает JWT, tenant-домен, userId, DOM, бизнес-данные или аудио после завершения STT; +- не является источником прав доступа; +- не генерирует исполняемый код или селекторы. + +## 8. Выбор технологического стека + +### 8.1. API-сервис + +Рекомендуемый стек: + +- Python 3.13 с последним patch-релизом как консервативная baseline-версия; переход на 3.14 — после проверки всей матрицы AI/audio-зависимостей; +- FastAPI; +- Pydantic v2 для строгих DTO и JSON Schema; +- Uvicorn как ASGI server; +- SQLAlchemy 2.x; +- Alembic для миграций отдельной БД ассистента; +- PostgreSQL 17 с последним доступным minor-релизом для новой отдельной инсталляции; если инфраструктурно сервис размещается в существующем PostgreSQL 16, использовать последний minor 16 и заранее проверить миграции; +- `httpx` для вызова внешних AI API; +- OpenTelemetry Python SDK; +- `structlog` или стандартный `logging` с JSON formatter; +- `pytest`, `pytest-asyncio`, `respx` и Testcontainers; +- `ruff` для lint/format; +- `mypy` или `pyright` в строгом режиме; +- `uv` и `uv.lock` для воспроизводимых зависимостей. + +Причины выбора: + +- зрелая экосистема AI, аудио и HTTP-интеграций; +- быстрый выпуск адаптеров к российским API; +- автоматическая OpenAPI-схема для генерации TypeScript-контрактов; +- удобная строгая валидация ответов модели; +- асинхронная обработка внешних API без блокировки worker; +- простое тестирование провайдеров через fake-адаптеры; +- независимость от Java backend Magistr. + +В Kubernetes каждый pod запускает один ASGI-процесс. Масштабирование выполняется репликами Deployment, а миграции запускаются отдельным одноразовым Job до rollout. Несколько worker внутри одного контейнера не являются базовой production-схемой. + +### 8.2. Виджет + +Рекомендуемый стек: + +- TypeScript со строгими настройками; +- Web Component с Shadow DOM; +- без React, Vue и других runtime-фреймворков; +- ESM-сборка через esbuild; +- CSS отдельным same-origin файлом; +- Web Audio API и MediaRecorder только для явной голосовой сессии; +- Vitest или `node:test` для unit-тестов; +- Playwright для браузерной интеграции; +- OpenAPI-generated типы для API; +- exact dependency versions и lock-файл. + +Причины выбора: + +- Magistr уже использует ES modules и Vanilla JavaScript; +- Web Component изолирует стили персонажа от страниц; +- небольшой размер bundle; +- отсутствие второго frontend-фреймворка в Magistr; +- возможность опубликовать виджет отдельным версионированным пакетом; +- Shadow DOM не мешает host bridge подсвечивать элементы основного документа. + +### 8.3. Почему не следует делать fork AIRI + +AIRI решает значительно более широкую задачу: самостоятельный виртуальный персонаж, локальные модели, память, игры, Live2D/VRM и множество runtime-окружений. Для Magistr полезны только идеи разделения UI, диалога, STT и TTS. Fork создаст большой объём лишних зависимостей и усложнит безопасность, CSP, обновления и поддержку. + +## 9. Предлагаемая структура отдельного репозитория + +```text +magistr-assistant/ +├── AGENTS.md +├── README.md +├── pyproject.toml +├── uv.lock +├── package.json +├── package-lock.json +├── Makefile +├── compose.yaml +├── .env.example +├── .gitea/ +│ └── workflows/ +│ ├── checks.yaml +│ ├── build-image.yaml +│ ├── publish-widget.yaml +│ └── deploy.yaml +├── service/ +│ └── assistant_api/ +│ ├── main.py +│ ├── api/ +│ │ ├── bootstrap.py +│ │ ├── chat.py +│ │ ├── voice.py +│ │ ├── feedback.py +│ │ ├── operator.py +│ │ └── health.py +│ ├── auth/ +│ │ ├── assistant_token.py +│ │ └── operator_auth.py +│ ├── domain/ +│ │ ├── entitlement.py +│ │ ├── interaction.py +│ │ ├── workflow.py +│ │ └── ui_action.py +│ ├── orchestration/ +│ │ ├── intent_resolver.py +│ │ ├── workflow_engine.py +│ │ ├── response_validator.py +│ │ └── language.py +│ ├── providers/ +│ │ ├── base.py +│ │ ├── fake.py +│ │ ├── llm.py +│ │ ├── stt.py +│ │ └── tts.py +│ ├── persistence/ +│ │ ├── models.py +│ │ ├── repositories.py +│ │ └── retention.py +│ ├── observability/ +│ │ ├── logging.py +│ │ ├── metrics.py +│ │ └── tracing.py +│ └── settings.py +├── migrations/ +│ ├── env.py +│ └── versions/ +├── widget/ +│ ├── src/ +│ │ ├── assistant-element.ts +│ │ ├── api-client.ts +│ │ ├── conversation-store.ts +│ │ ├── hold-to-talk.ts +│ │ ├── host-bridge.ts +│ │ ├── i18n.ts +│ │ ├── pending-action.ts +│ │ ├── renderer.ts +│ │ └── styles.css +│ ├── assets/ +│ │ └── placeholder-character/ +│ ├── tests/ +│ ├── tsconfig.json +│ └── esbuild.mjs +├── contracts/ +│ ├── site-map.schema.json +│ ├── workflow.schema.json +│ ├── ui-action.schema.json +│ ├── assistant-response.schema.json +│ └── generated/ +├── content/ +│ ├── site-map/ +│ │ └── magistr.v1.yaml +│ └── workflows/ +│ ├── calendar.set_non_working_day.yaml +│ ├── time_slots.edit_saturday.yaml +│ ├── schedule.view_group.yaml +│ └── schedule.move_lesson.yaml +├── deploy/ +│ ├── docker/ +│ └── kubernetes/ +├── tests/ +│ ├── contract/ +│ ├── integration/ +│ ├── retention/ +│ ├── security/ +│ └── provider/ +└── docs/ + ├── ARCHITECTURE.md + ├── API.md + ├── CONTENT_AUTHORING.md + ├── DEPLOYMENT.md + ├── PRIVACY.md + ├── RUNBOOK.md + └── THREAT_MODEL.md +``` + +`deploy/kubernetes` может содержать шаблоны нового проекта, но production source of truth должен быть согласован с текущим внешним инфраструктурным контуром. Kubernetes-файлы ассистента не следует добавлять в репозиторий Magistr. + +## 10. Модель интеграции релизов + +Новый репозиторий публикует два независимых артефакта: + +1. Docker image API-сервиса. +2. Версионированный ESM-пакет виджета. + +API развивается независимо при соблюдении обратной совместимости `/v1`. Magistr фиксирует точную версию widget-пакета и обновляет её отдельным pull request. Это исключает незаметную подмену исполняемого кода на странице без пересборки Magistr. + +Правила совместимости: + +- semantic versioning для API и widget; +- `apiVersion` в bootstrap-ответе; +- `siteMapVersion` в widget, API и контексте страницы; +- сервис поддерживает текущую и предыдущую minor-версию site map; +- несовместимая версия приводит к безопасному текстовому ответу без UI-действий; +- удаление action или target выполняется только в major-версии контракта; +- widget никогда не загружает исполняемый JavaScript с AI-провайдера. + +## 11. Контракт навигации и подсветки + +### 11.1. Site map + +Ассистент не должен угадывать устройство интерфейса по DOM. В отдельном репозитории хранится версионированная карта разрешённых маршрутов и целей. В Magistr ей соответствуют стабильные атрибуты `data-assistant-target` и функции host bridge. + +Пример элемента карты: + +```yaml +siteMapVersion: 1.0.0 +application: admin +routes: + - id: academic-calendar.grid + document: admin + tab: academic-calendar + roles: [ADMIN, EDUCATION_OFFICE] + readyEvent: magistr:view-mounted + targets: + - id: academic-calendar.inner-tab.grid + kind: static + - id: academic-calendar.graph-select + kind: static + - id: academic-calendar.load-grid + kind: static + - id: academic-calendar.day-cell + kind: resolver + acceptedParameters: [isoDate, course] +``` + +Site map содержит только интерфейсный контракт: маршруты, допустимые роли, цели, типы параметров и локализованные названия. В нём нет токенов, tenant-настроек, персональных данных и бизнес-содержимого страницы. + +### 11.2. Стабильные цели первого релиза + +При реализации в Magistr необходимо добавить стабильные цели минимум для следующих элементов: + +| Target ID | Назначение | +|---|---| +| `navigation.academic-calendar` | Переход к календарному учебному графику | +| `academic-calendar.inner-tab.grid` | Внутренняя вкладка «Сетки» | +| `academic-calendar.graph-select` | Выбор календарного графика | +| `academic-calendar.load-grid` | Загрузка сетки | +| `academic-calendar.day-cell` | Ячейка конкретной даты и курса через resolver | +| `academic-calendar.activity-select` | Выбор кода дня | +| `academic-calendar.save-grid` | Кнопка сохранения сетки | +| `navigation.settings` | Переход в отдельный SPA настроек | +| `settings.time-slots` | Вкладка временных слотов | +| `time-slots.scope-select` | Область действия сетки | +| `time-slots.slot-row` | Строка конкретной пары через resolver | +| `time-slots.edit-slot` | Кнопка изменения слота | +| `time-slots.save-slot` | Сохранение слота | +| `navigation.schedule-view` | Просмотр расписаний | +| `schedule-view.target-select` | Поле «Что смотреть» | +| `schedule-view.group-select` | Выбор группы | +| `schedule-view.semester-select` | Выбор семестра | +| `schedule-view.load` | Кнопка «Показать» | +| `schedule-view.lesson-card` | Карточка занятия через resolver | +| `schedule-view.edit-override` | Кнопка изменения занятия | +| `schedule-override.date` | Дата переноса | +| `schedule-override.time-slot` | Время занятия | +| `schedule-override.save` | Сохранение точечного изменения | + +Target ID является стабильным публичным контрактом. Он не должен совпадать с CSS-селектором и не должен раскрывать внутренние числовые идентификаторы. Если нужен динамический элемент, host bridge получает строго типизированные параметры и сам находит его в текущем представлении. + +### 11.3. Разрешённые UI-действия + +API может вернуть только следующие семантические действия: + +| Действие | Параметры | Эффект | +|---|---|---| +| `navigate` | `routeId` | Открыть разрешённый маршрут | +| `highlight` | `targetId`, необязательные типизированные параметры | Прокрутить к элементу и подсветить его | +| `showWorkflow` | `workflowId`, `currentStep` | Показать локализованные шаги в виджете | +| `clearHighlight` | нет | Снять предыдущую подсветку | +| `announce` | локализованный ключ | Озвучить короткое состояние через ARIA live region | + +Запрещены действия `click`, `type`, `submit`, `eval`, произвольный URL, произвольный CSS-селектор и произвольный JavaScript. Параметры, не предусмотренные JSON Schema, отклоняются. + +Пример безопасного ответа: + +```json +{ + "requestId": "019...", + "locale": "ru", + "answer": "Открою просмотр расписаний и покажу поле выбора группы.", + "workflow": { + "id": "schedule.view_group", + "version": "1.0.0", + "currentStep": 2 + }, + "actions": [ + {"type": "navigate", "routeId": "schedule-view"}, + {"type": "highlight", "targetId": "schedule-view.group-select"} + ], + "feedbackToken": "opaque-short-lived-token" +} +``` + +### 11.4. Выполнение действия на клиенте + +Host bridge выполняет последовательность: + +1. Проверяет JSON Schema, версию контракта, текущую роль и allowlist маршрута. +2. Выполняет разрешённую навигацию штатным роутером Magistr. +3. Ждёт событие `magistr:view-mounted` с ожидаемым `routeId` и `siteMapVersion`. +4. Находит target через зарегистрированный resolver. +5. Прокручивает элемент через `scrollIntoView` с учётом `prefers-reduced-motion`. +6. Добавляет CSS-класс подсветки и доступное пояснение. +7. Снимает подсветку по `Escape`, клику пользователя, смене маршрута или таймауту. + +Если цель не найдена за настраиваемый таймаут, клиент показывает шаги текстом, регистрирует обезличенное событие `target_not_found` и не пытается найти похожий элемент эвристикой. + +Переход между основной панелью и отдельным SPA настроек выполняется через `sessionStorage`. Разрешено сохранить только `actionId`, `routeId`, `targetId`, типизированные несекретные параметры, версию и срок жизни не более двух минут. Свободный текст запроса, токены и пользовательские данные там не сохраняются. + +## 12. Формат проверенных workflow + +Каждый поддерживаемый сценарий хранится в Git и проходит code review. Модель не придумывает инструкцию с нуля, а выбирает и адаптирует утверждённый workflow. + +```yaml +id: time_slots.edit_saturday +version: 1.0.0 +status: published +roles: [ADMIN, EDUCATION_OFFICE] +locales: + ru: + title: Изменить время пар в субботу + examples: + - Где менять время пар в субботу? + - Как изменить субботние звонки? + denied: Изменять временные слоты могут администратор и учебный отдел. + en: + title: Change Saturday lesson times + examples: + - Where can I change Saturday lesson times? +steps: + - id: open-settings + action: {type: navigate, routeId: settings.time-slots} + - id: choose-scope + action: {type: highlight, targetId: time-slots.scope-select} + requiresUserChoice: true + - id: choose-slot + action: {type: highlight, targetId: time-slots.slot-row} + requiresUserChoice: true + - id: save + action: {type: highlight, targetId: time-slots.save-slot} + irreversible: false +``` + +Обязательные поля workflow: + +- уникальный стабильный `id` и semver-версия; +- разрешённые роли; +- русские и английские примеры намерения; +- локализованный отказ по правам; +- последовательность шагов; +- явные точки, где требуется выбор пользователя; +- допустимые параметры и их типы; +- требуемая версия site map; +- дата проверки и ответственный редактор; +- тестовые фразы и ожидаемые действия; +- признак снятия с публикации без физического удаления старой версии. + +Контент публикуется вместе с API image или как подписанный неизменяемый bundle. Редактирование workflow через production-БД в первом релизе не требуется: Git обеспечивает review, историю и откат. + +## 13. Обработка текстового запроса и роль AI + +### 13.1. Конвейер + +1. Проверить assistant-токен и активную подписку tenant. +2. Нормализовать локаль, длину и Unicode запроса. +3. Удалить очевидные секреты по маскам только как дополнительную защиту; не обещать абсолютную деидентификацию произвольного текста. +4. Сопоставить запрос с проверенными примерами детерминированным поиском. +5. Если совпадение уверенное, выбрать workflow без вызова LLM. +6. Если запрос неоднозначен, отправить провайдеру минимальный текст и каталог разрешённых намерений для роли. +7. Принять от провайдера только структурированный результат по JSON Schema: `intentId`, извлечённые безопасные параметры, локаль и confidence. +8. Повторно проверить роль, workflow, параметры и site map на своей стороне. +9. Сформировать ответ из утверждённых локализованных шаблонов. +10. Вернуть действия клиенту и записать обезличенный outcome. + +LLM используется как классификатор и языковой слой, но не как источник прав, маршрутов или произвольных действий. Низкая уверенность приводит к уточняющему вопросу без навигации. Если вопрос не относится к поддерживаемым возможностям, ассистент честно сообщает границы первого релиза. + +### 13.2. Пороговые решения + +Пороги задаются конфигурацией и калибруются на тестовом наборе: + +- высокая уверенность: вернуть workflow; +- средняя уверенность: попросить выбрать из двух наиболее близких сценариев; +- низкая уверенность: ответить, что сценарий пока не поддерживается; +- конфликт роли и workflow: вернуть ролевой отказ независимо от confidence модели; +- конфликт версии: только текстовая инструкция без UI-действия. + +Нельзя использовать ответ провайдера, если в нём неизвестный `intentId`, target, лишнее поле, неправильный тип или параметр вне allowlist. + +### 13.3. Адаптеры провайдеров + +Для LLM, STT и TTS определяются отдельные интерфейсы. Один поставщик может реализовать все три функции, но код не должен это предполагать. Адаптер обязан поддерживать: + +- общий нормализованный DTO; +- таймауты подключения и ответа; +- повтор только безопасных идемпотентных запросов; +- circuit breaker; +- ограничение конкурентных запросов; +- унифицированные коды ошибок; +- учёт длительности, токенов/символов и стоимости без текста; +- fake-реализацию для локальной разработки и CI. + +Автоматический failover между разными AI-провайдерами не включается до юридической и качественной проверки: он способен незаметно изменить место обработки данных и поведение модели. + +## 14. Голосовой сценарий push-to-talk + +### 14.1. Состояния интерфейса + +```text +idle -> requesting_permission -> recording -> uploading -> transcribing + -> thinking -> answer_ready -> optional_speaking -> idle +``` + +Из любого промежуточного состояния возможен переход в `cancelled` или `error`, после которого пользователь может повторить попытку текстом. + +### 14.2. Правила записи + +- запись начинается только при `pointerdown` или доступном клавиатурном эквиваленте на кнопке; +- запись завершается при `pointerup`, `pointercancel`, потере capture, отпускании клавиши, скрытии вкладки или достижении лимита; +- рекомендуемый начальный лимит — 30 секунд и 10 МБ до серверной проверки; +- короткая запись ниже технического порога не отправляется; +- пользователь видит индикатор записи и длительность; +- повторное нажатие во время отправки не запускает параллельную запись; +- исходный аудиобуфер удаляется из памяти клиента после завершения или ошибки; +- сервер не пишет аудио во временный постоянный файл без необходимости; если конвертация требует файла, используется изолированный temp-каталог с гарантированным удалением; +- аудио никогда не попадает в логи, трассировки, аналитику или резервные копии. + +На сервере проверяются реальный контейнер/кодек, длительность, размер и частота. При необходимости отдельный sandboxed-процесс приводит запись к поддерживаемому моноформату с жёсткими лимитами CPU, памяти и времени. + +### 14.3. Распознавание и озвучивание + +STT возвращает транскрипт только в память текущего запроса. После классификации он удаляется. Пользователь видит распознанный текст и может отменить запрос до навигации. + +TTS по умолчанию озвучивает короткий ответ, только если пользователь включил голосовой ответ в текущей сессии. Для длинной пошаговой инструкции озвучивается резюме, полный текст остаётся на экране. Полученное аудио не кешируется межпользовательски, если лицензионные условия провайдера и модель угроз не разрешают безопасный кеш. + +Если браузер, политика безопасности или провайдер не поддерживает голос, текстовый режим остаётся полностью работоспособным. + +## 15. Локализация + +- локаль по умолчанию берётся из пользовательского интерфейса Magistr, а не определяется по имени пользователя; +- пользователь может переключить `Русский / English` внутри виджета на время текущей browser-сессии; +- ответ следует языку последнего запроса, если язык определён уверенно; +- UI, workflow, ролевые отказы, ошибки, ARIA-метки и голосовые подсказки существуют на обоих языках; +- отсутствие перевода является ошибкой сборки контента; +- fallback — русский для русской инсталляции, но технический ключ никогда не показывается пользователю; +- названия реальных элементов Magistr берутся из утверждённых переводов site map, а не переводятся моделью на лету; +- тестовый корпус содержит русские, английские и смешанные формулировки, опечатки и варианты транслитерации названий групп. + +## 16. Персонаж и UX виджета + +До выбора образа используется нейтральный абстрактный placeholder без человекоподобной мимики. Архитектура UI должна позволять заменить набор изображений и токенов темы без изменения диалоговой логики. + +Обязательные визуальные состояния: + +- ожидание; +- пользователь удерживает кнопку и говорит; +- распознавание речи; +- подготовка ответа; +- показ следующего шага; +- успешное нахождение элемента; +- элемент или функция недоступны; +- техническая ошибка с переходом на текстовый режим. + +Виджет располагается поверх интерфейса, но не перекрывает основные кнопки и системные уведомления. На небольшом экране он раскрывается в нижнюю панель. Диалог закрывается с клавиатуры; фокус не теряется; используется ARIA dialog/live region; контраст соответствует WCAG 2.2 AA; анимация отключается при `prefers-reduced-motion`. + +Образ не должен обещать полномочия, которых нет у ассистента. Формулировки первого релиза: «покажу», «подскажу», «открою раздел», но не «изменю» и не «сохраню». + +## 17. API-контракты + +Все публичные эндпоинты versioned, принимают и возвращают JSON, кроме аудио и SSE. Ошибки имеют машинный `code`, локализованный `message` и `requestId`. Внешняя документация не раскрывает operator API. + +### 17.1. Обмен токена в Magistr + +```http +POST /api/assistant/session +Authorization: Bearer <основной access token Magistr> +Content-Type: application/json + +{"locale":"ru","widgetVersion":"1.0.0","siteMapVersion":"1.0.0"} +``` + +Magistr проверяет обычную аутентификацию и роль, затем подписывает отдельный короткоживущий JWT. Пример ответа: + +```json +{ + "enabled": true, + "assistantBasePath": "/assistant/v1", + "token": "eyJ...", + "expiresAt": "2026-08-05T12:05:00Z" +} +``` + +Если подписка отключена, основной backend может вернуть `enabled: false` без токена. Это улучшает UX, но не заменяет обязательную серверную проверку подписки в новом сервисе. + +### 17.2. Bootstrap + +```http +GET /assistant/v1/bootstrap +Authorization: Bearer +``` + +Возвращает совместимые версии, доступные функции (`text`, `voiceInput`, `voiceOutput`, `feedback`), лимиты текущей сессии, локали и публичную конфигурацию персонажа. В ответе нет коммерческих сумм и секретной информации о tenant. + +### 17.3. Текстовый диалог + +```http +POST /assistant/v1/chat/stream +Authorization: Bearer +Content-Type: application/json +Accept: text/event-stream + +{ + "requestId": "019...", + "locale": "ru", + "message": "Как добавить выходной на этой неделе в четверг?", + "pageContext": { + "application": "admin", + "routeId": "schedule-view", + "siteMapVersion": "1.0.0" + } +} +``` + +SSE-события ограничены типами `status`, `answer`, `workflow`, `actions`, `done`, `error`. UI-действия отправляются только после полной серверной валидации, одним завершённым событием. Если реальный streaming не поддерживается выбранным провайдером, контракт сохраняется, но сервер отправляет один готовый ответ. + +Первая версия не принимает историю целиком. Виджет хранит ограниченный контекст текущей вкладки в памяти, а сервер получает только последние необходимые сообщения с жёстким лимитом и не сохраняет их. Для четырёх стартовых workflow предпочтительно обрабатывать каждый вопрос независимо. + +### 17.4. Голос + +```http +POST /assistant/v1/voice/transcriptions +Authorization: Bearer +Content-Type: multipart/form-data +``` + +Ответ содержит транскрипт, определённую локаль и request ID. Транскрипт возвращается пользователю, но не сохраняется сервисом. + +```http +POST /assistant/v1/voice/speech +Authorization: Bearer +Content-Type: application/json + +{"requestId":"019...","locale":"ru","textKey":"workflow.calendar.summary"} +``` + +Предпочтителен `textKey` с разрешёнными параметрами, а не произвольный длинный текст. Ответ — поток аудио с правильным `Content-Type`, `Cache-Control: no-store` и ограничением размера. + +### 17.5. Оценка + +```http +POST /assistant/v1/feedback +Authorization: Bearer +Content-Type: application/json + +{"feedbackToken":"opaque...","rating":"positive","reasonCode":"found_target"} +``` + +`rating`: `positive` или `negative`. Свободный комментарий в первом релизе отсутствует, чтобы случайно не сохранять текст диалога или персональные данные. Допустимые причины выбираются из локализованного списка. + +### 17.6. Operator API + +Минимальный закрытый набор: + +- `PUT /internal/v1/tenants/{tenantKey}/entitlement` — включить или выключить доступ, задать дату окончания и квоты; +- `GET /internal/v1/tenants/{tenantKey}/entitlement` — проверить состояние; +- `GET /internal/v1/tenants/{tenantKey}/usage` — получить агрегаты без текстов; +- `POST /internal/v1/tenants/{tenantKey}/rotate-feedback-key` — аварийная ротация; +- `POST /internal/v1/retention/run` — только для диагностического ручного запуска защищённой job. + +На первом этапе вместо отдельного operator UI допустима CLI-команда, вызывающая тот же application service. Ручное изменение строк SQL считается аварийной операцией, а не штатным управлением. + +### 17.7. Служебные эндпоинты + +- `GET /health/live` — процесс работает, без проверки внешнего AI; +- `GET /health/ready` — доступна БД, загружены workflow и ключи проверки JWT; +- `GET /metrics` — только во внутренней сети; +- OpenAPI в production закрыта или доступна только оператору. + +Недоступность внешнего AI-провайдера не должна постоянно выводить pod из readiness и провоцировать рестарт. Она отражается отдельной метрикой, circuit breaker и понятной ошибкой пользователю. + +## 18. Аутентификация и границы доверия + +Assistant JWT подписывается асимметричным ключом Magistr. Новый сервис получает только публичный ключ или JWKS. + +Рекомендуемые claims: + +```json +{ + "iss": "magistr", + "aud": "magistr-assistant", + "sub": "opaque-session-subject", + "tenant": "university-key", + "role": "EDUCATION_OFFICE", + "departmentId": null, + "locale": "ru", + "jti": "019...", + "iat": 1785931200, + "exp": 1785931500 +} +``` + +Правила: + +- срок жизни — около пяти минут; +- токен не продлевается сервисом ассистента; новый получает только Magistr; +- `aud`, `iss`, подпись, `exp`, допустимая роль и tenant обязательны; +- email, ФИО, логин и основной access token не передаются; +- `departmentId` нужен только для ролевого варианта кафедры; +- публичные ключи ротируются с перекрывающим периодом по `kid`; +- токен хранится только в памяти виджета, не в `localStorage`; +- ответы API имеют `Cache-Control: no-store`; +- CORS не открывается глобально, основной путь same-origin; +- rate limit применяется по tenant, сессии и типу операции; +- operator-доступ использует отдельную аутентификацию, сетевое ограничение и аудит. + +Новый сервис принимает решение о доступности подсказки, но не заменяет авторизацию Magistr. Даже будущие исполняемые действия должны повторно проверяться backend Magistr. + +## 19. Модель данных отдельной БД + +### 19.1. `tenant_entitlements` + +| Поле | Назначение | +|---|---| +| `tenant_key` | Первичный ключ, нормализованный tenant | +| `status` | `enabled`, `suspended`, `expired` | +| `contract_reference` | Внутренняя ссылка на договор, без текста договора | +| `starts_at`, `ends_at` | Период доступа | +| `features` | Строго валидируемые флаги text/STT/TTS | +| `quota_policy` | Ссылка на версионированную политику квот | +| `created_at`, `updated_at` | Служебные даты | + +Эта коммерческая конфигурация не является пользовательской аналитикой и хранится до окончания договорных и бухгалтерских обязательств. Она не удаляется 30-дневной job. + +### 19.2. `usage_events` + +Разрешённые поля: + +- UUID события и время; +- tenant key; +- роль из ограниченного enum; +- тип канала: text/STT/TTS; +- `intentId` или `unsupported`, но не текст запроса; +- версия workflow, site map, widget и API; +- outcome: `success`, `clarification`, `denied`, `target_not_found`, `provider_error`; +- нормализованная латентность; +- количество входных/выходных токенов, символов или секунд аудио; +- код провайдера из внутреннего enum; +- технический request ID; +- `expires_at = created_at + 30 days`. + +Запрещённые поля: prompt, транскрипт, ответ, аудио, имя группы, дата занятия, ФИО, email, логин, основной user ID, IP, User-Agent, DOM и URL с query-параметрами. + +### 19.3. `feedback_events` + +Содержит tenant, `intentId`, workflow version, `positive/negative`, выбранный reason code, channel, created/expiry timestamps и одноразовый псевдослучайный идентификатор ответа. Свободного комментария нет. Нельзя восстановить текст вопроса по feedback token. + +### 19.4. `operator_audit_events` + +Фиксирует включение/выключение подписки, изменение срока и квот: оператор, действие, tenant, время, old/new state без секретов. Для аудита управления срок хранения задаётся отдельно согласно договорным требованиям; 30-дневное удаление пользовательской аналитики на эту таблицу не распространяется. Это исключение должно быть явно отражено в политике конфиденциальности. + +### 19.5. Агрегаты и квоты + +Для быстрых лимитов допустима таблица дневных/месячных счётчиков без пользователя. Агрегаты после удаления событий можно хранить дольше только если они необратимо укрупнены минимум до tenant + месяц + тип операции и не содержат request ID, роли малой группы или редких `intentId`. До юридического решения безопасный вариант — удалять и события, и производные пользовательские агрегаты через 30 дней. + +Векторная БД в MVP не нужна: поддерживаемых workflow мало, их версии лежат в Git, а детерминированный поиск проще проверять. + +### 19.6. Retention job + +- ежедневный Kubernetes CronJob или внутренний scheduled worker с distributed lock; +- удаление пакетами по индексированному `expires_at`, чтобы не блокировать таблицу; +- отдельные индексы на `expires_at`, `(tenant_key, created_at)` и поля агрегирования; +- метрики времени последнего успешного запуска, количества удалённых строк и oldest remaining event; +- alert, если старейшая запись превысила 31 день; +- интеграционный тест с искусственно устаревшими строками; +- резервные копии пользовательской аналитики имеют согласованный срок жизни и не превращают удаление из основной БД в фикцию; +- восстановление backup сопровождается немедленным повторным retention до открытия трафика. + +## 20. Безопасность и приватность + +### 20.1. Основные угрозы + +| Угроза | Контроль | +|---|---| +| Межтенантный доступ | Tenant только из проверенного JWT; entitlement и квоты запрашиваются с ним; тесты на несовпадающие tenant | +| Повышение роли через prompt | Роль только из JWT; модель не влияет на policy engine | +| Prompt injection | Провайдер выбирает только intent из allowlist; DOM и произвольные инструкции сайта ему не отправляются | +| XSS в ответе | Строгая схема, `textContent`, запрет HTML/Markdown с raw HTML, CSP | +| Произвольное управление UI | Только semantic actions и target allowlist; нет `click`, selector и script | +| Кража токена | Короткий TTL, хранение в памяти, same-origin, `no-store`, строгая CSP | +| Обход отключённой подписки | Проверка entitlement на каждом billable запросе и при bootstrap | +| Перерасход API | Квоты, rate limits, maximum concurrency, circuit breaker и бюджеты | +| Аудиобомба/вредный контейнер | Проверка magic bytes, лимиты размера/длительности, sandboxed transcoding | +| Утечка через логи/трейсы | Централизованный allowlist полей и тест, запрещающий content keys | +| Устаревшая карта UI | Версионирование, contract test и безопасный текстовый fallback | +| Компрометация operator API | Отдельная identity, private network, least privilege, audit, ротация ключей | +| Сбой AI-провайдера | Таймаут, circuit breaker, текстовая ошибка, Magistr продолжает работать | + +### 20.2. Минимизация данных + +Перед подключением реального провайдера документируются регион обработки, срок хранения входных данных провайдером, использование запросов для обучения, порядок удаления и перечень субподрядчиков. В настройках провайдера выбирается режим без обучения и с минимальным техническим хранением, если он доступен. + +Пользователю перед первой голосовой записью кратко сообщается, что звук отправляется внешнему сервису для распознавания и не сохраняется Magistr Assistant. Разрешение браузера запрашивается только после нажатия на кнопку микрофона. + +Нельзя обещать полную анонимность текста, который пользователь сам ввёл: он может написать ФИО или другую персональную информацию. Поэтому интерфейс предупреждает не вводить персональные данные, сервис не хранит текст, а провайдер выбирается с подходящими договорными условиями. + +### 20.3. Секреты и сеть + +- ключи AI API, приватный ключ operator auth и DSN БД хранятся в Kubernetes Secret или внешнем secret store; +- секреты не попадают в image, репозиторий, frontend bootstrap и логи; +- egress разрешён только к БД, telemetry collector и выбранным provider endpoints; +- ingress к публичному API идёт только через reverse proxy; operator и metrics routes закрыты NetworkPolicy; +- TLS используется для внешних вызовов и соединения с БД в production; +- зависимости и container image регулярно сканируются; +- административная ротация секретов описывается в runbook и проверяется до пилота. + +## 21. Подписка, квоты и управление оператором + +### 21.1. Жизненный цикл подписки + +1. Оператор получает подтверждение договора вне системы. +2. Проверяет точный `tenantKey` университета. +3. Через защищённую CLI или operator API задаёт период, функции и квоты. +4. Система показывает оператору итоговое состояние до применения. +5. После подтверждения создаётся audit event. +6. Widget становится доступен при следующем bootstrap без релиза Magistr. +7. При `suspended` или истечении срока новые запросы блокируются сервером; Magistr продолжает работать. +8. Возобновление подписки не восстанавливает удалённую аналитику. + +Статусы должны иметь точные причины: договор завершён, ручная приостановка, квота исчерпана, конфигурация неполна. Конечному пользователю не показываются сведения о договоре; он получает нейтральное сообщение и предложение обратиться к ответственному лицу университета. + +### 21.2. Квоты + +Квоты конфигурируются отдельно для: + +- текстовых запросов; +- секунд STT; +- символов или секунд TTS; +- одновременных запросов tenant; +- burst rate на одну browser-сессию. + +Числовые значения нельзя фиксировать до получения тарифов и лимитов выбранных российских API. До пилота необходимо посчитать стоимость на тестовом корпусе и задать мягкий warning-порог и жёсткий hard limit. Проверка квоты и резервирование использования должны быть атомарными. При ошибке внешнего провайдера списывается только фактически тарифицированное использование, насколько это позволяет его API. + +### 21.3. Operator CLI первого релиза + +Предлагаемые команды: + +```text +assistantctl tenant show +assistantctl tenant enable --until YYYY-MM-DD --policy pilot-v1 +assistantctl tenant suspend --reason contract-ended +assistantctl tenant set-features --text on --stt on --tts on +assistantctl tenant usage --period 30d +assistantctl retention status +``` + +CLI обязана иметь `--dry-run`, подтверждение потенциально блокирующих изменений и машиночитаемый JSON-режим. Секрет оператора не передаётся аргументом командной строки. + +## 22. Наблюдаемость и эксплуатационные цели + +### 22.1. Логи + +Production-логи структурированы, а человекочитаемые сообщения написаны по-русски. Разрешённые поля: timestamp, level, service/version, environment, requestId, traceId, tenant key, роль-enum, endpoint, intent ID, outcome, latency bucket, provider code и error code. + +Запрещённые поля централизованно фильтруются: `message`, `prompt`, `transcript`, `answer`, `audio`, Authorization, cookie, JWT, ФИО, email, IP, User-Agent и multipart body. Логирование request/response body в reverse proxy и APM выключается для `/assistant/*`. + +### 22.2. Метрики + +Минимальный набор: + +- `assistant_requests_total{channel,outcome,role}`; +- `assistant_request_duration_seconds{channel,stage}`; +- `assistant_provider_requests_total{provider,capability,outcome}`; +- `assistant_provider_duration_seconds{provider,capability}`; +- `assistant_usage_units_total{tenant,capability}` с контролем cardinality; +- `assistant_workflow_matches_total{intent,outcome}`; +- `assistant_ui_actions_total{action,outcome}`; +- `assistant_target_not_found_total{site_map_version,target}`; +- `assistant_feedback_total{rating,reason,intent}`; +- `assistant_entitlement_denied_total{reason}`; +- `assistant_retention_last_success_timestamp`; +- `assistant_retention_oldest_event_age_seconds`; +- состояние circuit breaker и очереди запросов. + +Для большого числа tenant допускается убирать tenant label из Prometheus и строить коммерческие отчёты запросом к БД, чтобы не создавать высокую cardinality. + +### 22.3. Трассировки + +Span-цепочка: reverse proxy → auth/entitlement → intent resolution → provider → response validation. Текстовые и аудиоданные никогда не записываются в span attributes или events. Трассировка интегрируется с существующим OpenTelemetry/SigNoz контуром, но сервис имеет собственное имя и dashboards. + +### 22.4. Начальные SLO для пилота + +| Показатель | Цель | +|---|---:| +| Доступность API подсказок | 99,5% за месяц, исключая согласованные работы | +| Bootstrap p95 внутри инфраструктуры | до 300 мс | +| Готовый текстовый ответ p95 | до 8 с | +| STT + ответ без TTS p95 для записи до 15 с | до 15 с | +| Выполнение локальной навигации и подсветки p95 после ответа | до 1 с после готовности view | +| Успешное нахождение target на поддерживаемой версии UI | не менее 98% | +| Пользовательская аналитика старше 30 дней | 0 строк, допустимое окно job до 24 часов | +| Влияние аварии ассистента на Magistr | 0 отказов основных функций | + +Это стартовые ориентиры, а не договорной SLA. После пилота они уточняются по фактическим latency российских провайдеров и инфраструктуры. + +### 22.5. Оповещения и runbook + +Alerts: + +- резкий рост 5xx или provider errors; +- p95 выше цели в течение согласованного окна; +- circuit breaker открыт длительное время; +- retention не выполнялся более 25 часов; +- есть запись старше 31 дня; +- истекает ключ JWT/JWKS или provider secret; +- квота tenant близка к hard limit; +- несовместимость site map/widget; +- массовый `target_not_found` после релиза Magistr. + +Runbook должен описывать диагностику, безопасное отключение только ассистента, переключение на текстовый режим, ротацию секретов, повтор retention, откат image и widget, а также правила обращения с потенциальной утечкой. + +## 23. Развёртывание и CI/CD отдельного проекта + +### 23.1. Контейнер + +- multi-stage build; +- непривилегированный пользователь и read-only root filesystem; +- минимальный базовый образ с фиксированным digest; +- `no-new-privileges`, drop Linux capabilities; +- отдельный writable temp volume только если нужен аудиоконвертер; +- health endpoints без секретов; +- graceful shutdown длиннее максимального обычного запроса; +- SBOM и vulnerability scan; +- OCI labels с git SHA и версией; +- image публикуется по immutable digest. + +### 23.2. Kubernetes + +Production-набор: + +- Deployment API минимум с двумя репликами для пилотной production-среды; +- Service; +- Ingress/reverse-proxy route `/assistant/*` на доменах Magistr; +- одноразовый migration Job перед rollout; +- CronJob retention с distributed lock; +- ConfigMap только для несекретных параметров; +- Secret/external secret; +- NetworkPolicy ingress/egress; +- PodDisruptionBudget; +- resource requests/limits; +- topology spread или anti-affinity при наличии нескольких узлов; +- HPA только после появления достоверных метрик CPU/concurrency; +- ServiceMonitor/OTel configuration; +- отдельный service account без доступа к Kubernetes API, если он не нужен. + +Внешний инфраструктурный репозиторий остаётся source of truth для production-манифестов. Изменение текущего `../k8s` выполняется отдельной задачей и не является частью создания этого документа. + +### 23.3. Reverse proxy и CSP + +Рекомендуемый маршрут same-origin: + +```text +https:///assistant/v1/* -> magistr-assistant-service +``` + +Путь обмена токена остаётся в Magistr: `/api/assistant/session`. Виджет и CSS отдаются Magistr как локальные versioned assets. Для микрофона политика меняется с `microphone=()` на `microphone=(self)`. Новые внешние домены в browser CSP не добавляются, потому что виджет обращается только к same-origin proxy. + +### 23.4. Gitea Actions + +Pipeline для pull request: + +1. Проверка format/lint Python и TypeScript. +2. Строгая проверка типов. +3. Unit-тесты. +4. Валидация JSON Schema и всех YAML workflow. +5. Проверка полноты русских/английских переводов. +6. Contract-тест с зафиксированной версией Magistr bridge. +7. Интеграционные тесты PostgreSQL и fake providers. +8. Playwright для виджета. +9. Security tests, dependency audit, secret scan. +10. Сборка API image и widget без публикации. + +Pipeline `main` дополнительно публикует артефакты, SBOM и immutable digest. Production deploy запускается только после успешных проверок, миграции и принятой стратегии окружений. `workflow_dispatch` остаётся дополнительным способом повторить безопасный deploy, а не единственным способом доставки. + +### 23.5. Локальная разработка + +`compose.yaml` нового репозитория поднимает API, PostgreSQL, fake LLM/STT/TTS и при необходимости OTel collector. Magistr подключается через локальный proxy или dev override. По умолчанию никакой запрос не уходит во внешний AI API; реальный provider включается явной переменной и отдельным профилем. + +## 24. Стратегия тестирования + +### 24.1. Unit-тесты + +- role policy для каждого workflow; +- confidence thresholds; +- валидация и отклонение неизвестных actions/targets; +- вычисление квот; +- entitlement status и даты; +- локализация и fallback; +- очистка лог-полей; +- TTL pending action; +- state machine push-to-talk; +- provider timeout/retry/circuit breaker; +- формирование `expires_at`. + +### 24.2. Contract-тесты + +- OpenAPI ↔ generated TypeScript types; +- workflow ↔ workflow schema; +- target из workflow обязательно существует в site map; +- role workflow не шире role target/route; +- Magistr bridge поддерживает все action types; +- widget/API/site map version matrix; +- все опубликованные строки имеют RU и EN; +- удалённый target требует major change или deprecation window. + +Contract bundle полезно публиковать отдельным package, который подключают оба репозитория. CI Magistr должен падать, если новый UI удалил требуемую цель без согласованного обновления контракта. + +### 24.3. Интеграционные тесты API + +- валидный/истёкший/неверно подписанный JWT; +- неверные `iss`, `aud`, tenant и роль; +- включённая, выключенная и истёкшая подписка; +- атомарное исчерпание квоты конкурентными запросами; +- отсутствие текста и аудио в БД, логах и traces; +- ошибки/таймауты/невалидный JSON каждого provider; +- очистка временного аудио после успеха, отмены и исключения; +- retention на границе 30 дней; +- восстановление из backup с повторной очисткой; +- недоступность provider не ломает health/live и Magistr. + +### 24.4. Browser E2E + +Для каждой из трёх ролей и двух локалей проверяются: + +- загрузка/скрытие виджета по entitlement; +- клавиатурное открытие и закрытие; +- переход в SPA и между документами; +- ожидание асинхронной готовности представления; +- подсветка, прокрутка, снятие по Escape и таймауту; +- безопасный fallback, когда target отсутствует; +- отсутствие исполнения HTML в вредоносном ответе; +- запрет action, недоступного роли; +- сохранение основного функционала Magistr при 5xx/timeout API; +- permission denied для микрофона; +- отпускание кнопки за пределами элемента и скрытие вкладки; +- `prefers-reduced-motion`, масштаб 200%, мобильная ширина и screen reader labels. + +### 24.5. Приёмочные сценарии MVP + +| Фраза | Роль | Ожидаемый результат | +|---|---|---| +| «Как добавить выходной на этой неделе в четверг?» | ADMIN/EDUCATION_OFFICE | Открыта сетка календаря, подсвечен выбор графика, показаны дальнейшие шаги | +| Та же фраза | DEPARTMENT | Понятный ролевой отказ, запрещённый экран не открыт | +| «Где менять время пар в субботу?» | ADMIN/EDUCATION_OFFICE | Открыты временные слоты, подсвечена область действия | +| Та же фраза | DEPARTMENT | Указана ответственная роль | +| «Какое расписание у группы ИБ-01б?» | ADMIN/EDUCATION_OFFICE | Открыт просмотр расписания, подсвечено поле группы | +| Та же фраза | DEPARTMENT | Открыт собственный режим кафедры, объяснено ограничение | +| «Как перенести пару с одной даты на другую?» | ADMIN/EDUCATION_OFFICE | Открыт просмотр, объяснён выбор занятия и override | +| Та же фраза | DEPARTMENT | Указана ответственная роль, редактирование не предлагается | + +Для каждой строки создаются английские варианты, голосовая версия, вариант с опечатками и негативный тест, в котором модель пытается вернуть запрещённое действие. + +### 24.6. Нагрузочные и отказные тесты + +- bootstrap storm после начала рабочего дня; +- параллельные короткие text-запросы; +- максимальные разрешённые аудиозаписи; +- медленный provider и разрыв SSE; +- временная недоступность БД; +- истощение connection pool; +- остановка pod во время запроса; +- одновременный retention и чтение агрегатов; +- поведение при исчерпании tenant quota; +- доказательство, что деградация ассистента не увеличивает latency основных API Magistr. + +## 25. Поэтапный план реализации + +Оценки ниже даны в человеко-днях для одного разработчика, включают код и основные тесты, но не ожидание договора с AI-провайдером, юридическое согласование, закупку и создание финальной графики. Диапазон нужен для планирования, а не является обещанием срока. + +### Этап 0. Проработка и ADR — 3–5 дней + +Задачи: + +- создать отдельный репозиторий и его `AGENTS.md`; +- утвердить рабочее имя и владельца сервиса; +- сравнить российские LLM/STT/TTS API по SLA, регионам, retention, цене и качеству русского/английского; +- утвердить data flow и договорные ограничения; +- зафиксировать ADR по стеку, same-origin proxy, token exchange, workflow engine и отсутствию server-side history; +- снять базовые замеры четырёх сценариев вручную; +- выбрать начальные SLO и бюджет пилота. + +Критерий выхода: выбран provider или согласован fake-first режим, утверждены границы данных и архитектурные решения. + +### Этап 1. Контракты интерфейса Magistr — 5–8 дней + +Задачи: + +- оформить JSON Schema actions/site map/workflow; +- внедрить в Magistr host bridge и `data-assistant-target` только для четырёх сценариев; +- добавить событие готовности асинхронных views; +- реализовать безопасную навигацию и pending action между SPA; +- добавить CSS подсветки и accessibility; +- создать fake widget для проверки без AI; +- добавить contract и browser tests для ролей. + +Критерий выхода: статический тестовый ответ безопасно открывает и подсвечивает каждый разрешённый target, а запрещённые actions отклоняются. + +### Этап 2. Основа сервиса, auth и entitlement — 8–12 дней + +Задачи: + +- создать FastAPI skeleton, конфигурацию и русские логи; +- PostgreSQL/Alembic schema; +- assistant token endpoint в Magistr и JWKS/rotation contract; +- проверка JWT и tenant entitlement; +- operator CLI с audit; +- квоты и rate limits; +- bootstrap, health, metrics; +- retention job и тесты; +- fake provider. + +Критерий выхода: включённый tenant получает bootstrap, выключенный блокируется сервером, межтенантные тесты и retention проходят. + +### Этап 3. Виджет и текстовый UX — 10–15 дней + +Задачи: + +- Web Component, адаптивная панель и placeholder персонажа; +- текстовый ввод, ответы, шаги и оценки; +- browser-only conversation state; +- SSE client, timeout/cancel/retry; +- bridge actions и безопасный fallback; +- RU/EN UI; +- CSP integration; +- unit и Playwright tests. + +Критерий выхода: все четыре workflow полностью проходят с fake provider текстом на двух языках. + +### Этап 4. AI-классификация и контент — 8–12 дней + +Задачи: + +- детерминированный resolver; +- адаптер выбранного LLM; +- structured output и строгая повторная валидация; +- confidence/clarification policy; +- все четыре versioned workflow; +- тестовый корпус фраз и regression evaluation; +- бюджеты, timeout и circuit breaker; +- защита логов и аналитика outcome. + +Критерий выхода: целевые метрики качества достигнуты на зафиксированном тестовом корпусе, запрещённые действия не проходят validator. + +### Этап 5. Голос и завершение локализации — 8–12 дней + +Задачи: + +- hold-to-talk state machine; +- браузерные разрешения и CSP/Permissions-Policy; +- STT adapter, проверка и ограничение аудио; +- просмотр транскрипта и отмена; +- TTS adapter и управление воспроизведением; +- двуязычные голоса и fallback; +- accessibility и мобильные сценарии; +- тесты удаления буферов и временных файлов. + +Критерий выхода: голосовой вопрос можно безопасно задать удержанием кнопки, а отказ любого voice-компонента оставляет рабочий текстовый режим. + +### Этап 6. Production hardening — 8–12 дней + +Задачи: + +- threat-model review; +- dependency/container scan и SBOM; +- Kubernetes policies/resources/migration/retention jobs; +- dashboards, alerts и runbooks; +- нагрузочные и failure tests; +- backup/restore/retention rehearsal; +- секреты и ротация; +- проверка отсутствия content в logs/traces/DB; +- rollback rehearsal API и widget. + +Критерий выхода: закрыты критические угрозы, проверены аварийные процедуры, сервис можно отключить без влияния на Magistr. + +### Этап 7. Пилот одного университета — 5–10 дней активной работы + +Задачи: + +- включить подписку одному tenant; +- провести короткое обучение ответственных пользователей; +- следить за latency, target failures, квотами и оценками; +- вручную разобрать негативные оценки только по обезличенным кодам и воспроизвести сценарии на тестовом стенде; +- исправить контент/target contracts; +- получить решение о расширении. + +Критерий выхода: все четыре сценария устойчивы, retention подтверждён, нет влияния на основные функции, согласованы следующие workflow. + +### Итоговая ориентировочная трудоёмкость + +Полный путь до завершённого пилота: примерно **55–86 человеко-дней**. При одном разработчике это обычно 3–5 календарных месяцев с учётом согласований и исправлений; при параллельной работе backend/frontend/infra календарный срок можно уменьшить, но контракты этапа 1 и privacy-решения этапа 0 остаются последовательными зависимостями. + +## 26. Декомпозиция начального бэклога + +### Контракты и Magistr + +- `ASST-001` — ADR архитектуры и границ данных. +- `ASST-002` — schema semantic UI actions. +- `ASST-003` — schema site map и workflow. +- `ASST-004` — inventory target для четырёх сценариев. +- `ASST-005` — stable `data-assistant-target` в основной admin SPA. +- `ASST-006` — targets в settings SPA. +- `ASST-007` — host bridge role/route allowlist. +- `ASST-008` — `magistr:view-mounted` и ожидание view. +- `ASST-009` — pending action между документами. +- `ASST-010` — CSS highlight и accessibility. +- `ASST-011` — assistant session endpoint и асимметричные ключи. +- `ASST-012` — CSP и `Permissions-Policy` для same-origin voice. + +### Service core + +- `ASST-013` — FastAPI skeleton, settings и error model. +- `ASST-014` — PostgreSQL/Alembic и repository layer. +- `ASST-015` — JWT/JWKS validator. +- `ASST-016` — tenant entitlement application service. +- `ASST-017` — quota/rate-limit service. +- `ASST-018` — operator CLI и audit. +- `ASST-019` — bootstrap API. +- `ASST-020` — feedback tokens и endpoint. +- `ASST-021` — usage events без content. +- `ASST-022` — 30-day retention job. +- `ASST-023` — health, metrics, traces и log allowlist. + +### Workflow и AI + +- `ASST-024` — provider abstractions и fake provider. +- `ASST-025` — deterministic intent resolver. +- `ASST-026` — LLM adapter и structured output. +- `ASST-027` — response/action validator. +- `ASST-028` — confidence/clarification policy. +- `ASST-029` — workflow календарного выходного. +- `ASST-030` — workflow субботних временных слотов. +- `ASST-031` — workflow просмотра группы. +- `ASST-032` — workflow переноса занятия. +- `ASST-033` — RU/EN regression corpus. + +### Widget и voice + +- `ASST-034` — Web Component shell и placeholder. +- `ASST-035` — text chat и browser state. +- `ASST-036` — SSE client/cancel/retry. +- `ASST-037` — workflow step renderer. +- `ASST-038` — bridge client и action executor. +- `ASST-039` — feedback UI. +- `ASST-040` — i18n completeness gate. +- `ASST-041` — hold-to-talk state machine. +- `ASST-042` — STT upload/validation/adapter. +- `ASST-043` — TTS playback/adapter. +- `ASST-044` — voice privacy notice и permissions UX. + +### Delivery и эксплуатация + +- `ASST-045` — Gitea checks pipeline. +- `ASST-046` — immutable API image и SBOM. +- `ASST-047` — versioned widget publication. +- `ASST-048` — Kubernetes templates/infrastructure integration. +- `ASST-049` — dashboards и alerts. +- `ASST-050` — privacy/security/runbook docs. +- `ASST-051` — load/failure/security test suite. +- `ASST-052` — pilot checklist и rollback rehearsal. + +## 27. Definition of Done первого релиза + +Релиз считается готовым, только если одновременно выполнено следующее: + +- отдельный репозиторий, pipeline и независимые версии API/widget существуют; +- один tenant можно включить и выключить без нового релиза; +- сервер проверяет tenant, роль, entitlement и квоту на каждом запросе; +- роли `ADMIN`, `EDUCATION_OFFICE`, `DEPARTMENT` проходят согласованную матрицу; +- четыре стартовых вопроса работают текстом и голосом на русском и английском; +- ассистент выполняет только навигацию и подсветку; +- ни модель, ни клиент не могут вернуть/выполнить неизвестный selector, URL или action; +- ответ при отсутствии прав понятен и не открывает запрещённый раздел; +- полный текст, транскрипт и аудио отсутствуют в БД, логах, traces и backups; +- оценка ответа хранится без свободного текста и удаляется с аналитикой через 30 дней; +- retention проверен автоматическим тестом и наблюдаемой production job; +- voice permission запрашивается только после осознанного действия; +- сбой API/LLM/STT/TTS не мешает пользоваться Magistr; +- CSP, XSS, межтенантные и quota-тесты проходят; +- browser E2E подтверждает targets на текущей версии Magistr; +- есть dashboard, alerts, runbook, rollback и операторский аудит; +- документация API, контента, privacy, deployment и эксплуатации актуальна; +- пилотный университет и оператор приняли пользовательские сценарии. + +## 28. Основные риски и способы снижения + +| Риск | Вероятность/влияние | Снижение | +|---|---|---| +| UI Magistr меняется и подсветка ломается | Высокая/среднее | Stable target contract, CI двух репозиториев, version fallback | +| Модель выбирает неверный сценарий | Средняя/высокое | Детерминированный слой, ограниченный каталог, confidence и clarification | +| Российский API имеет высокую задержку | Средняя/среднее | Замеры этапа 0, короткие prompts, timeout, UX states, provider abstraction | +| Стоимость голоса выше ожиданий | Средняя/высокое | Hold-to-talk, лимит длины, квоты, TTS opt-in, замер unit economics | +| Пользователь произносит персональные данные | Высокая/среднее | Не хранить content, предупреждение, договорные настройки provider, минимальная передача | +| Отсутствуют права на нужный экран | Высокая/низкое | Ролевой workflow как штатный результат, без обходов | +| Межтенантная утечка | Низкая/критическое | Подписанный tenant claim, central policy, isolation tests, no business API access | +| Operator случайно отключает не тот tenant | Средняя/высокое | Exact tenant preview, dry-run, confirmation и audit | +| Два независимых релиза несовместимы | Средняя/высокое | Semver, compatibility window, contract package и безопасный fallback | +| Retention не охватывает backup | Средняя/высокое | Короткий backup retention, restore procedure с cleanup, регулярная проверка | +| Виджет ухудшает доступность интерфейса | Средняя/среднее | Keyboard/screen-reader/mobile tests, reduced motion, неперекрывающая компоновка | +| Fork большого avatar-проекта усложняет поддержку | Высокая/среднее | Малый Web Component, заимствовать идеи, не кодовую базу AIRI | + +## 29. Подготовка к будущему выполнению действий + +Возможность «сделать после подтверждения» не следует реализовывать расширением текущего `highlight` до `click`. Для неё нужен отдельный command-контур: + +```text +пользовательский запрос + -> draft command + -> серверная проверка прав и актуального состояния + -> человекочитаемый preview «что изменится» + -> явное подтверждение пользователя + -> короткоживущий confirmation token + -> идемпотентная команда Magistr API + -> серверный audit/result +``` + +Обязательные будущие свойства: + +- отдельные backend endpoints Magistr, не DOM-автоматизация; +- allowlist команд, строгие DTO и повторная обычная RBAC-проверка; +- preview без побочных эффектов; +- подтверждение привязано к exact command hash, tenant, пользователю и короткому TTL; +- изменение параметра аннулирует старое подтверждение; +- idempotency key и защита от повторной отправки; +- optimistic locking/version для изменяемого объекта; +- явный показ «было / станет»; +- усиленное подтверждение для массовых и необратимых действий; +- полный служебный audit по требованиям безопасности, отдельно от диалогового текста; +- компенсация или понятная ручная процедура, если команда выполнена частично; +- отдельный threat model и юридическое согласование. + +До появления этого контура UI action schema первого релиза не содержит `click`, `fill` и `submit` даже в скрытом виде. + +## 30. Открытые решения до начала разработки + +Эти вопросы не блокируют создание плана, но должны быть закрыты на этапе 0: + +1. Какой российский provider выбран отдельно для LLM, STT и TTS; можно ли юридически использовать одного поставщика для всех трёх функций? +2. Какие у него фактические retention, region, SLA, rate limits, streaming и режим запрета обучения? +3. Какие квоты и себестоимость допустимы для пилотного договора? +4. Нужен ли голосовой ответ по умолчанию или только после отдельного включения пользователем? +5. Какой tenant станет пилотным и сколько одновременно активных пользователей ожидается? +6. Каким способом operator CLI аутентифицируется в production: mTLS, OIDC service identity или иной внутренний механизм? +7. Где будет source of truth для production manifest нового сервиса и как он связан с текущим внешним `k8s`? +8. Каков точный срок хранения operator audit и договорных записей? +9. Нужно ли хранить укрупнённые месячные коммерческие агрегаты дольше 30 дней, и можно ли доказать их необратимую обезличенность? +10. Какой окончательный образ, имя, голос, цветовая тема и допустимый уровень анимации у единого персонажа? +11. Нужен ли пользователю переключатель полного выключения озвучивания и удаления browser-session контекста одной кнопкой? +12. Кто принимает и обновляет двуязычный workflow-контент после пилота? + +## 31. Рекомендуемый порядок следующих действий + +1. Согласовать этот документ как продуктовую и архитектурную основу. +2. Выбрать пилотный tenant и получить обезличенное число потенциальных пользователей. +3. Провести короткий provider spike на четырёх фразах и их вариантах, не передавая production-данные. +4. Зафиксировать ADR и JSON Schema контрактов. +5. Создать отдельный репозиторий `magistr-assistant`. +6. Первым техническим инкрементом реализовать fake assistant + Magistr bridge без внешнего AI. +7. Только после стабильной навигации добавить token exchange, entitlement и API. +8. Затем подключить LLM; после текстовой приёмки — STT/TTS. +9. Выполнить security/privacy review и production hardening. +10. Включить один tenant, собрать 30 дней обезличенных исходов и решить, какие workflow добавлять дальше. + +## 32. Источники и точки сверки + +### Исходники Magistr + +- `frontend/admin/js/main.js` — маршрутизация и асинхронная загрузка views; +- `frontend/admin/js/role-capabilities.js` — доступные вкладки по ролям; +- `frontend/admin/settings/js/main.js` — отдельный settings SPA; +- `frontend/admin/js/views/academic-calendar.js` — сетка календаря и выбор активности дня; +- `frontend/admin/settings/js/views/time-slots.js` — временные слоты и области действия; +- `frontend/admin/js/views/schedule-view.js` — режим группы, кафедры и override; +- `backend/src/main/java/com/magistr/app/controller/AcademicCalendarController.java` — роли календаря; +- `backend/src/main/java/com/magistr/app/controller/ScheduleOverrideController.java` — роли точечных изменений; +- `frontend/security.conf` — текущие CSP и Permissions-Policy; +- `docs/ARCHITECTURE.md`, `docs/FRONTEND.md`, `docs/INFRASTRUCTURE.md` — действующие проектные соглашения. + +### Внешняя техническая документация + +- [FastAPI: общие принципы deployment](https://fastapi.tiangolo.com/deployment/concepts/) — репликация, перезапуски, память и предварительные шаги; +- [FastAPI: server workers](https://fastapi.tiangolo.com/deployment/server-workers/) — модель worker и контейнерные варианты; +- [Статус версий Python](https://devguide.python.org/versions/) — сроки bugfix/security-поддержки 3.13 и 3.14; +- [PostgreSQL: политика версий](https://www.postgresql.org/support/versioning/) — поддерживаемые major/minor и сроки окончания поддержки; +- [SQLAlchemy 2.0 Documentation](https://docs.sqlalchemy.org/en/20/) — выбранная стабильная линия ORM. + +Перед непосредственной реализацией версии языка, библиотек, PostgreSQL, browser APIs и требования конкретного AI-провайдера необходимо повторно проверить по актуальной официальной документации и зафиксировать lock-файлами. diff --git a/DEVOPS.md b/DEVOPS.md deleted file mode 100644 index 05a37cf..0000000 --- a/DEVOPS.md +++ /dev/null @@ -1,292 +0,0 @@ -# РАЗРАБОТКА И ВНЕДРЕНИЕ ОТКАЗОУСТОЙЧИВОЙ МУЛЬТИТЕНАНТНОЙ ИНФРАСТРУКТУРЫ ДЛЯ СИСТЕМЫ УПРАВЛЕНИЯ УНИВЕРСИТЕТСКИМ РАСПИСАНИЕМ «МАГИСТР» - -**Диссертационное исследование и отчет о проделанной инженерной работе в качестве DevOps-архитектора проекта** - ---- - -## ВВЕДЕНИЕ - -Современные информационные системы для образовательных учреждений требуют строгого соблюдения требований к безопасности, изоляции данных различных организаций (университетов), гибкости масштабирования и высокой наблюдаемости (observability). В рамках разработки проекта **«Магистр»** — системы управления расписанием учебных занятий — передо мной встала задача проектирования и построения инфраструктуры, способной обслуживать десятки университетов в рамках единого прикладного контура при условии жесткой изоляции их баз данных. - -В качестве DevOps-инженера проекта я спроектировал и реализовал архитектурное решение, объединяющее технологии аппаратной виртуализации, оркестрации контейнеров, автоматизации конфигурации, распределенного мониторинга и непрерывной интеграции. В данном документе подробно описаны теоретические предпосылки, практическая реализация и архитектурные решения, внедренные мной в продакшн-окружение проекта. - ---- - -## РАЗДЕЛ 1. СИСТЕМНАЯ АРХИТЕКТУРА И КОНЦЕПЦИЯ МУЛЬТИТЕНАНТНОСТИ - -### 1.1 Выбор паттерна изоляции данных -При проектировании мультитенантных (multi-tenant) систем классически выделяют три подхода к организации баз данных: -1. **Shared Database & Shared Schema**: Все клиенты используют общие таблицы, записи разделяются по полю `tenant_id`. Подход дешев в обслуживании, но несет колоссальные риски утечки данных из-за ошибок в SQL-запросах приложения и не позволяет разграничивать физический доступ к базам. -2. **Shared Database & Separate Schemas**: Одна СУБД, но разные логические схемы для каждого клиента. Обеспечивает умеренную изоляцию, но сохраняет единую точку отказа и общие аппаратные ресурсы СУБД. -3. **Database-per-Tenant (Выбранный подход)**: Каждый клиент (университет) владеет собственной физически изолированной базой данных, расположенной на выделенном сервере или виртуальной машине. - -Для проекта «Магистр» мной был выбран и реализован паттерн **Database-per-Tenant**. Это обусловлено спецификой клиентов (высшие учебные заведения), которые согласно законодательству (например, ФЗ-152 «О персональных данных») обязаны хранить свои данные локально или требовать полной изоляции конфиденциальной информации. Кроме того, данный подход позволяет: -* Исключить влияние высокой нагрузки одного университета (например, в период составления сессий) на доступность системы для других вузов («синдром шумного соседа»). -* Выполнять индивидуальное резервное копирование и обслуживание БД каждого университета без остановки всей платформы. -* Переносить базы данных вузов на их собственные физические серверы в локальных сетях (on-premise), сохраняя при этом работу приложения в центральном облаке. - -### 1.2 Архитектурная схема движения трафика -Ниже представлена разработанная мной схема прохождения сетевых запросов и агрегации телеметрии в системе: - -```mermaid -graph TD - Client[Пользовательский браузер] -->|HTTPS: tenant.zuev.company| Caddy[Caddy Reverse Proxy] - Caddy -->|HTTP Round-Robin| Ingress[Traefik Ingress Controller] - - subgraph K3s Cluster - Ingress -->|Route /*| Frontend[Frontend Pods x2 Apache] - Ingress -->|Route /api/*| Backend[Backend Pods x1-x2 Spring Boot] - Backend -->|K8s API: Get/Patch CM| K8sAPI[Kubernetes API Server] - BackendConfig[ConfigMap: tenants-config] -.->|Mounted JSON| Backend - end - - subgraph Proxmox VE Infrastructure - Backend -->|JDBC Connection| VM1[(VM 1: DBMS MSU
PostgreSQL 16)] - Backend -->|JDBC Connection| VM2[(VM 2: DBMS SWSU
PostgreSQL 16)] - - VM1 -.->|Metrics OTLP/gRPC| Collector1[OTel Collector VM1] - VM2 -.->|Metrics OTLP/gRPC| Collector2[OTel Collector VM2] - end - - subgraph Observability Hub - Backend -->|Logs & Traces OTLP| SigNoz[SigNoz APM Server] - Collector1 -->|Postgres Metrics| SigNoz - Collector2 -->|Postgres Metrics| SigNoz - end - - classDef cluster fill:#e1f5fe,stroke:#01579b,stroke-width:2px; - classDef vm fill:#efebe9,stroke:#4e342e,stroke-width:2px; - classDef monitor fill:#efe8ff,stroke:#512da8,stroke-width:2px; - class K3s Cluster cluster; - class Proxmox VE Infrastructure vm; - class Observability Hub monitor; -``` - ---- - -## РАЗДЕЛ 2. АППАРАТНАЯ ВИРТУАЛИЗАЦИЯ НА БАЗЕ PROMVOX VE - -Для размещения баз данных клиентов и вспомогательных инфраструктурных служб мной был развернут гипервизор **Proxmox Virtual Environment (PVE)** на выделенных серверных мощностях. - -### 2.1 Конфигурация виртуализации и изоляции ресурсов -Каждая база данных университета разворачивается в выделенной виртуальной машине (KVM), что гарантирует жесткую изоляцию на уровне ядра ОС и гипервизора. -* **Сетевой уровень**: В Proxmox настроена виртуальная коммутация (Linux Bridge `vmbr0`). Сетевые интерфейсы виртуальных машин баз данных вынесены в приватную подсеть, изолированную от внешней сети. Доступ к ним имеет только подсеть кластера Kubernetes и управляющая машина администратора. -* **Дисковая подсистема**: Использовано локальное хранилище на базе ZFS с зеркалированием (RAID-10 на SSD-накопителях). Это обеспечивает: - * Высокую скорость операций ввода-вывода (IOPS), критически важную для СУБД при транзакционных нагрузках. - * Механизм мгновенных снимков (snapshots) для безопасного проведения обновлений. - * Аппаратное сжатие данных (LZ4) без потери производительности, снижающее износ накопителей. -* **Резервное копирование**: Интегрирован **Proxmox Backup Server (PBS)**. Каждую ночь выполняются инкрементальные бэкапы виртуальных машин с дедупликацией на уровне блоков данных, что минимизирует нагрузку на сеть и диски, позволяя восстановить любую БД на любой момент времени за последние 30 дней. - ---- - -## РАЗДЕЛ 3. ОРКЕСТРАЦИЯ КОНТЕЙНЕРНОЙ ИНФРАСТРУКТУРЫ В KUBERNETES (K3s) - -В качестве платформы оркестрации приложений был выбран **K3s** — сертифицированный дистрибутив Kubernetes от Rancher, оптимизированный под средние и малые нагрузки, обладающий минимальными накладными расходами на RAM/CPU для Control Plane, но сохраняющий полную совместимость с upstream-спецификациями Kubernetes API. - -### 3.1 Архитектура манифестов и структура ресурсов -Мной была разработана декларативная структура ресурсов, развернутая в изолированном пространстве имен `magistr` (манифест `namespace.yaml`): - -* **Frontend (`frontend.yaml`)**: - * Реализован в виде `Deployment` с 2 репликами для обеспечения высокой доступности (HA) и возможности бесшовного обновления rolling-update. - * В качестве базового образа контейнера применен легковесный веб-сервер Apache HTTPd; версия и manifest digest закреплены в Dockerfile. - * Для балансировки и внутреннего доступа настроен `Service` типа ClusterIP, слушающий порт 80. - -* **Backend (`backend.yaml`)**: - * `Deployment` с 1-2 репликами (детали балансировки конфигурации описаны ниже). - * Для сборки образов используется multi-stage сборка Maven (JDK 17); build/runtime-образы одновременно закреплены точным tag и manifest digest. - * Интегрирован Java-агент OpenTelemetry для автоматического инструментирования трассировки и логов. - * Для связи с Ingress настроен ClusterIP-сервис на порту 8080. - -* **Ingress (`ingress.yaml`)**: - * В роли Ingress-контроллера выступает встроенный **Traefik** (`kube-system/traefik`). - * Он доступен извне через Klipper Service LoadBalancer (DaemonSet), который прокидывает порты 80/443 (hostPort) на внешние IP адреса нод кластера (`192.168.1.104`, `192.168.1.105`, `192.168.1.106`). - * Мной настроены строгие правила маршрутизации по доменным именам (например, `magistr.zuev.company` и `n8n.zuev.company`). Запросы к API (`/api`) проксируются на бэкенд, а к корню (`/`) — на фронтенд. - -### 3.2 Реализация динамической мультитенантности через K8s API -Одним из наиболее сложных этапов проектирования стала организация бесшовного добавления новых университетов без перезапуска бэкенда и изменения исходного кода приложения. - -1. **Хранение конфигурации**: - Список подключений к базам данных университетов хранится в JSON-формате (`tenants.json`) внутри Kubernetes `ConfigMap` с именем `tenants-config`. - -2. **Проблема Read-Only монтирования**: - По умолчанию Kubernetes монтирует ConfigMap как файловую систему в режиме Read-Only. Это исключало возможность для Java-приложения перезаписывать `tenants.json` при запросах от администратора на добавление нового тенанта. - -3. **Решение с использованием `initContainer` и `emptyDir`**: - Я спроектировал схему с временным перезаписываемым томом (`emptyDir`): - * В спецификацию пода backend добавлен `initContainer` на базе образа `busybox`. - * При старте пода `initContainer` монтирует ConfigMap `tenants-config` и копирует файл `tenants.json` во временный раздел `emptyDir` (путь `/config`). - * Основной контейнер `backend` монтирует этот же `emptyDir` в режиме Read-Write. - -4. **K8s RBAC и динамическое сохранение**: - Чтобы предоставить бэкенду возможность сохранять изменения конфигурации обратно в кластер, мной были написаны манифесты авторизации (`rbac.yaml`): - * Создан `ServiceAccount` `backend-sa` и назначен поду бэкенда. - * Описана Kubernetes `Role` `backend-configmap-role`, дающая права только на операции `get` и `patch` для ресурса `configmaps` с конкретным именем `tenants-config` (принцип наименьших привилегий). - * Создан `RoleBinding` `backend-configmap-binding`, связывающий сервис-аккаунт с этой ролью. - - В коде бэкенда класс `ConfigMapUpdater` через Kubernetes Java Client отправляет PATCH-запрос к API-серверу при добавлении нового тенанта, обновляя ConfigMap в самом кластере. - -5. **Репликация и синхронизация подов**: - Для обеспечения консистентности данных в условиях работы нескольких реплик бэкенда: - * Класс `TenantConfigWatcher` запускает фоновую задачу, которая каждые 30 секунд сверяет MD5-хеш локального файла `tenants.json` с версией из ConfigMap. - * При обнаружении расхождения файл перезаписывается, DataSource-инстансы пересоздаются в памяти приложения на лету, исключая рассинхронизацию подов. - * *Примечание*: В ходе тестирования для минимизации задержек и исключения эффекта "мигания" данных на время отладки количество реплик backend было ограничено до 1, с последующим переходом на полноценный StatefulSet/Shared Storage при росте количества нод. - -### 3.3 Повышение отказоустойчивости бэкенда (Resiliency) -При запуске бэкенда в облаке возникала проблема: если база данных одного из университетов становилась недоступной (например, из-за сетевого сбоя или регламентных работ), пул подключений HikariCP падал с ошибкой при инициализации бинов, отправляя под бэкенда в циклическую перезагрузку (`CrashLoopBackOff`), что делало недоступной систему для *всех* остальных вузов. - -Для предотвращения этого каскадного сбоя я внедрил комплекс защитных механизмов: -* **H2 Fallback**: В сборку (`pom.xml`) добавлена СУБД H2 in-memory. Если при старте ConfigMap пуст, Spring Boot инициализирует пустой JPA-контекст на базе H2, что позволяет приложению успешно пройти фазу запуска. -* **HikariCP Resiliency**: В конфигурации источников данных задано свойство `setInitializationFailTimeout(-1)`. Это заставляет HikariCP игнорировать отсутствие связи с целевой БД при старте приложения, продолжая попытки подключения в фоновом режиме. -* **Явное указание диалекта Hibernate**: Принудительно задан `org.hibernate.dialect.PostgreSQLDialect` в `TenantDataSourceConfig.java`. Это избавило Hibernate от необходимости выполнять тестовый запрос к БД для автоматического определения диалекта на ранних этапах инициализации контекста. -* **Автоматическое создание таблиц (`DataInitializer`)**: Для новых БД вузов полностью автоматизирован процесс наката структуры. При обнаружении новой базы Java-код проверяет наличие таблицы `users` и, в случае ее отсутствия, самостоятельно применяет SQL-скрипт инициализации (`init.sql`), включая создание дефолтных справочников и учетной записи суперадминистратора. - ---- - -## РАЗДЕЛ 4. АВТОМАТИЗАЦИЯ КОНФИГУРАЦИИ СУБД С ПОМОЩЬЮ ANSIBLE - -Для развертывания и администрирования баз данных университетов на удаленных виртуальных машинах Proxmox мной был спроектирован и реализован комплексный плейбук **Ansible** с модульной структурой ролей. - -### 4.1 Структура и функционал Ansible-ролей -Вся конфигурация целевого сервера разбита на 6 последовательных этапов (плейбук `site.yml`): - -1. **`common` (Базовая подготовка и Hardening ОС)**: - * Создание системного пользователя `deploy` с беспарольным доступом к `sudo` (через валидацию файла `/etc/sudoers.d/deploy` утилитой `visudo`). - * Установка системных утилит (`curl`, `git`, `htop`, `chrony` для синхронизации времени по NTP). - * **SSH Hardening**: Изменение стандартного SSH-порта на `2222`, отключение авторизации по паролям, запрет входа для пользователя `root`, ограничение таймаута сессий. - * Адаптация под различные дистрибутивы ОС: написаны отдельные сценарии для семейств Debian/Ubuntu/Astra Linux (пакетный менеджер `apt`), RedHat/РЕД ОС (`dnf`) и ALT Linux (кастомная обертка для `apt-get`). - -2. **`docker` (Среда контейнеризации)**: - * Автоматическое добавление официальных GPG-ключей и репозиториев Docker CE. - * Установка Docker Engine, CLI и Compose плагина. - * Включение системной службы `docker` и добавление пользователя `deploy` в группу `docker` для беспарольного запуска контейнеров. - -3. **`firewall` (Сетевая безопасность)**: - * Настройка межсетевого экрана UFW (для Debian-based систем) или firewalld (для RedHat и ALT Linux). - * Полная блокировка входящего трафика по умолчанию. - * Разрешение входящих пакетов только на порт SSH (`2222`) и порт PostgreSQL (`5432`). Реализована поддержка ограничения доступа к порту `5432` только для IP-адресов нод Kubernetes-кластера (`firewall_postgresql_allowed_ips`). - -4. **`postgresql` (Контейнеризированная СУБД)**: - * Генерация файлов конфигурации `docker-compose.yml` и `.env` на основе шаблонов Jinja2. - * Развертывание PostgreSQL 16 на базе легковесного Alpine-образа. - * Физическое монтирование каталога данных БД с хост-системы (`/opt/magistr/postgres/data`), предотвращающее потерю данных при пересоздании контейнера. - * Ограничение системных ресурсов контейнера (limits: memory: 512M) и тюнинг параметров PostgreSQL (`shared_buffers=256MB`, оптимизация пула соединений, логирование медленных запросов `log_min_duration_statement=1000`). - -5. **`backup` (Резервное копирование на уровне БД)**: - * Установка на хост автоматического bash-скрипта бэкапа. - * Настройка задачи `cron` (запуск ежедневно в 03:00). - * Скрипт осуществляет горячий дамп базы через `docker exec pg_dump`, архивирует его с помощью `gzip` и сохраняет в локальный каталог `/opt/magistr/backups/`. - * Реализован алгоритм автоматической ротации: удаление архивных файлов старше заданного количества дней (по умолчанию 14 дней). - -6. **`monitoring` (Сбор телеметрии хоста и БД)**: - * Развертывание контейнера OpenTelemetry Collector на целевом сервере БД. - * Конфигурирование коллектора на сбор метрик производительности PostgreSQL и ОС (процессор, память, диски) и их отправку на единый сервер SigNoz по OTLP/HTTP. - -### 4.2 Управление секретами: Ansible Vault -Для исключения попадания паролей администраторов БД в систему контроля версий (Git) все чувствительные переменные (например, `vault_postgresql_password`) вынесены в файл `group_vars/databases/vault.yml` и зашифрованы алгоритмом AES-256 с помощью **Ansible Vault**. - -Для бесшовной интеграции в CI/CD пароль расшифрования считывается из локального файла `.vault_pass` на управляющей машине, доступ к которому ограничен правами `chmod 600`. - ---- - -## РАЗДЕЛ 5. НЕПРЕРЫВНАЯ ИНТЕГРАЦИЯ И ДОСТАВКА (CI/CD) - -В рамках оптимизации внутренних процессов разработки и деплоя мной была развернута и настроена инфраструктура непрерывной интеграции на базе **Gitea** и **Gitea Actions**. - -### 5.1 Автоматизация сборки (CI) -В репозитории проекта создан workflow-манифест `.gitea/workflows/docker-build.yaml`. При каждом пуше изменений в ветку `main` запускается конвейер: -1. **Checks**: Backend/frontend-тесты, Compose validation, тест immutable rollback и статическая проверка закрепления артефактов. -2. **Build & Push**: Параллельная сборка и публикация backend/frontend с SHA-tag или release tag; mutable `latest` не создаётся. -3. **Attestations**: BuildKit добавляет к обоим образам SBOM и максимальную provenance-attestation. -4. **Security scan**: Закреплённый digest Trivy блокирует доставку при исправимых уязвимостях `HIGH`/`CRITICAL`. -5. **Deploy**: Только прошедшие gates registry digests передаются в production rollout. Все сторонние Actions закреплены полными commit SHA. - -### 5.2 Доставка в кластер (CD) -Для авторизации нод K3s в приватном реестре Gitea мной был создан секрет `gitea-registry` типа `docker-registry` в пространстве имен `magistr`. Этот секрет ассоциирован со спецификациями деплоев через директиву `imagePullSecrets`. - -Непосредственно Gitea Actions Runner работает как демон `act_runner` внутри изолированного LXC контейнера (CTID 107). В пайплайне шаг развертывания (`deploy-to-k8s`) динамически генерирует `kubeconfig` из секрета, проверяет checksum закреплённой версии `kubectl` и выполняет атомарное обновление обоих образов без использования тяжеловесных GitOps операторов: - -```bash -bash scripts/deploy-images.sh \ - gitea.zuev.company/zuev/magistr-backend@sha256: \ - gitea.zuev.company/zuev/magistr-frontend@sha256: -``` - -Скрипт принимает только полные `image@sha256:...`, ожидает готовность обоих Deployment и при -ошибке возвращает предыдущую пару digest. Поэтому содержимое релиза не зависит от повторного -разрешения mutable tag. - ---- - -## РАЗДЕЛ 6. ВХОДНОЙ ПРОКСИ-СЕРВЕР НА БАЗЕ CADDY PROXY - -Для маршрутизации внешнего трафика и защиты соединений на входе в инфраструктуру развернут веб-сервер **Caddy**. - -### 6.1 Преимущества Caddy и управление SSL/TLS -В отличие от классического Nginx, Caddy был выбран благодаря: -* Встроенной интеграции с удостоверяющими центрами Let's Encrypt и ZeroSSL. При добавлении нового поддомена вуза (например, `swsu.zuev.company`) Caddy автоматически запрашивает, валидирует через DNS/HTTP-вызовы и устанавливает TLS-сертификат, а также следит за его продлением. -* Лаконичному синтаксису конфигурации (`Caddyfile`). -* Полноценной поддержке протокола HTTP/3 «из коробки». - -### 6.2 Конфигурация балансировки -Внешний Caddy-сервер развернут в изолированном LXC контейнере (CTID 101) и обрабатывает запросы ко всем поддоменам проекта. Он перенаправляет их на ноды кластера K3s, выполняя роль внешнего балансировщика, а также проксирует телеметрию с фронтенда напрямую в сборщик OTel: - -```caddyfile -(otel_proxy) { - handle_path /otel/* { reverse_proxy 192.168.1.100:4318 } -} - -(k8s_nodes) { - handle { - reverse_proxy 192.168.1.104:80 192.168.1.105:80 192.168.1.106:80 { - lb_policy round_robin - lb_try_duration 5s - lb_try_interval 250ms - } - } -} - -magistr.zuev.company { import otel_proxy; import k8s_nodes } -n8n.zuev.company { import otel_proxy; import k8s_nodes } -``` -Сетевые запросы к приложениям распределяются по 3 физическим нодам K3s. Благодаря сниппетам, добавление нового тенанта в инфраструктуре Caddy требует всего двух строчек конфигурации. - ---- - -## РАЗДЕЛ 7. СКВОЗНОЙ МОНИТОРИНГ И ОБСЕРВАБИЛИТИ (SigNoz + OpenTelemetry) - -Для контроля здоровья системы и оперативного выявления аномалий мной была спроектирована и внедрена централизованная система мониторинга на базе APM-платформы **SigNoz** (с хранилищем **ClickHouse**) и стандартов **OpenTelemetry (OTel)**. - -Платформа SigNoz вынесена в отдельный LXC контейнер (CTID 100) по адресу `192.168.1.100` и развернута через `docker-compose`. Данные ClickHouse и SQLite метаданные персистентно хранятся в Docker volumes хоста для обеспечения сохранности при рестартах. - -### 7.1 Сбор телеметрии Backend -Java-приложение бэкенда запускается с подключением агента OpenTelemetry (`opentelemetry-javaagent.jar`). Это позволяет без изменения кода собирать: -* **Трейсы (Traces)**: Сквозное прохождение HTTP-запросов через контроллеры, сервисы и JDBC-драйвер к базам данных. -* **Метрики (Metrics)**: Метрики JVM (утилизация Heap, активность сборщика мусора GC, состояние потоков), метрики HTTP-запросов (интенсивность, латентность, коды ответов). -* **Логи (Logs)**: Системные логи Logback экспортируются по протоколу OTLP напрямую в SigNoz. - -### 7.2 Идентификация тенантов в мониторинге -Для глубокого анализа производительности СУБД конкретных университетов критически важно разделять телеметрию по тенантам. Мной была реализована сквозная маркировка: -* В бэкенде через Java-интерцептор `TenantInterceptor` при каждом запросе идентификатор тенанта помещается в контекст логирования SLF4J MDC (`MDC.put("tenant.id", tenant)`) и одновременно записывается в атрибуты текущего OpenTelemetry спана (`Span.current().setAttribute("tenant.id", tenant)`). -* Благодаря переменной окружения `OTEL_INSTRUMENTATION_LOGBACK_APPENDER_EXPERIMENTAL_CAPTURE_MDC_ATTRIBUTES=tenant.id`, OTel-агент автоматически парсит MDC и прикрепляет его к логам, отправляемым в SigNoz. Это позволяет администраторам фильтровать ошибки и строить графики нагрузки в разрезе каждого университета. - -### 7.3 Мониторинг баз данных на хостах -Для сбора детальной статистики с изолированных серверов баз данных: -1. На хостах БД силами Ansible разворачивается OpenTelemetry Collector. -2. В конфигурации коллектора (`otel-collector-config.yml.j2`) описывается ресивер `postgresql`, который подключается к локальной БД и считывает системные метрики (размер таблиц, cache hit ratio, количество транзакций, активные сессии, блокировки). -3. К собираемым метрикам жестко прикрепляются ресурсные атрибуты хоста: `service.name=magistr-db-` и `university.name=`. -4. Метрики экспортируются в центральный коллектор SigNoz. В результате в интерфейсе SigNoz доступны кастомные дашборды JVM, PostgreSQL и HTTP, позволяющие оперативно локализовать проблемы с производительностью баз данных конкретных вузов. - ---- - -## ЗАКЛЮЧЕНИЕ - -Разработанная и внедренная мной DevOps-архитектура для проекта «Магистр» успешно решила задачи изоляции данных, отказоустойчивости и автоматизации администрирования. - -### Ключевые результаты проделанной работы: -1. **Изоляция данных**: Реализована физическая изоляция БД университетов на выделенных серверах (VM в Proxmox VE), соответствующая требованиям безопасности и законодательства. -2. **Динамическое масштабирование**: Внедрен механизм динамического добавления тенантов без простоя системы (zero-downtime) за счет связки Spring Boot, K8s RBAC и ConfigMapWatcher. -3. **Отказоустойчивость**: Минимизированы риски каскадных сбоев бэкенда за счет применения in-memory fallback баз данных и оптимизации параметров подключения HikariCP. -4. **Автоматизация**: Создан универсальный Ansible-плейбук для быстрого ввода в эксплуатацию новых серверов БД (время развертывания «под ключ» сокращено до нескольких минут). -5. **Наблюдаемость**: Реализован сквозной мониторинг логов, метрик и трассировок с детализацией по тенантам на базе стека OpenTelemetry + SigNoz. - -Созданная инфраструктура обладает высокой степенью гибкости и готова к дальнейшему горизонтальному масштабированию по мере подключения новых высших учебных заведений к проекту «Магистр». diff --git a/STARTUP_GUIDE.md b/STARTUP_GUIDE.md deleted file mode 100644 index 7937139..0000000 --- a/STARTUP_GUIDE.md +++ /dev/null @@ -1,162 +0,0 @@ -# Инструкция запуска Magistr - -Ниже — минимальный порядок действий. Подробности по инфраструктуре и секретам находятся в -[`docs/INFRASTRUCTURE.md`](docs/INFRASTRUCTURE.md) и -[`docs/SECURITY_RUNBOOK.md`](docs/SECURITY_RUNBOOK.md). - -## 1. Локальный запуск - -Нужны Git, Docker Engine, Docker Compose и запущенный Caddy из `../сaddy-proxy/`. - -1. В корне проекта создайте локальный файл настроек: - - ```bash - cp .env.example .env - openssl rand -base64 36 # значение POSTGRES_PASSWORD - openssl rand -base64 48 # значение JWT_SECRET - ``` - - Вставьте полученные значения в `.env`. Файл `.env` не коммитьте. - -2. Один раз создайте общую Docker-сеть, затем запустите Caddy и Magistr: - - ```bash - docker network inspect proxy >/dev/null 2>&1 || docker network create proxy - docker compose -f ../сaddy-proxy/compose.yaml up -d - docker compose config --quiet - docker compose up -d --build - docker compose ps - ``` - -3. Откройте `https://localhost` (`http://localhost` перенаправит на HTTPS). Проверьте backend - через Caddy: - - ```bash - curl -kfsS https://localhost/actuator/health/readiness - ``` - - Если браузер не доверяет локальному сертификату Caddy, на CachyOS/Arch выполните: - - ```bash - docker cp caddy:/data/caddy/pki/authorities/local/root.crt /tmp/caddy-local-root.crt - sudo trust anchor --store /tmp/caddy-local-root.crt - ``` - - До установки проверьте SHA-256 отпечаток командой - `openssl x509 -in /tmp/caddy-local-root.crt -noout -fingerprint -sha256`, затем полностью - перезапустите браузер. - - Для первого локального входа: `admin` / `admin`. Эти данные предназначены только для - разработки. - -4. Если запуск не удался: - - ```bash - docker compose logs --tail=200 backend db frontend - ``` - - Полный локальный сброс БД выполняется командой `docker compose down -v`, но она удалит - все локальные данные. Обычная остановка без удаления данных: `docker compose down`. - -## 2. Обновление существующего production - -Push в `main` автоматически выполняет проверки, собирает и сканирует образы, а затем -обновляет backend/frontend в Kubernetes по проверенным immutable digest. Новые файлы -`../k8s` CI не применяет: первый rollout и изменения самих манифестов выполняются -оператором через `../k8s/deploy.sh`. Gitea 1.25 игнорирует `environment` и `concurrency`, -поэтому они не используются как production-защита. - -1. Подготовьте секреты по [`docs/SECURITY_RUNBOOK.md`](docs/SECURITY_RUNBOOK.md): - - - `app-secret`: новый случайный `JWT_SECRET` не короче 32 байт, `POSTGRES_USER`, - `POSTGRES_PASSWORD`; - - `tenants-secret`: новый `tenants.json` с актуальными JDBC URL и ротированными паролями; - - `otel-postgres-secret`: восемь обязательных параметров подключений Collector; - - `gitea-registry`: доступ K3s к registry. - - Для `magistr.zuev.company` нужен tenant с `"domain": "magistr"`, для - `n8n.zuev.company` — `"domain": "n8n"`. Секреты не сохраняйте в Git или CI-логах. - -2. Выполните локальные проверки, затем отправьте изменения в `main`: - - ```bash - K8S_DIR=../k8s bash scripts/check-production-secrets.sh - K8S_DIR=../k8s bash scripts/check-artifact-pinning.sh - bash scripts/test-deploy-images.sh - kubectl kustomize ../k8s >/dev/null - ``` - - Дождитесь успешных checks, сборки и Trivy scan. Скопируйте точные backend/frontend - ссылки вида `image@sha256:...` из CI/registry. - -3. Объявите техническое окно и временно закройте публичный доступ во внешнем Caddy. Затем - остановите backend: - - ```bash - kubectl scale deployment/backend -n magistr --replicas=0 - ``` - - Полностью пересоздайте **каждую** PostgreSQL tenant-БД, указанную в новом - `tenants.json`. Недостаточно удалить `flyway_schema_history` или отдельные таблицы: - миграции V2–V7 объединены в новую `V1__init.sql`, поэтому старая БД получит checksum - error, а непустая БД без истории может быть ошибочно принята Flyway за baseline. - Пользователь БД должен уметь создавать схему, функции, триггеры и расширения - `pgcrypto`/`btree_gist`. - -4. С рабочей станции с актуальным каталогом `../k8s` выполните первый rollout вручную: - - ```bash - export BACKEND_IMAGE_REF='gitea.zuev.company/zuev/magistr-backend@sha256:' - export FRONTEND_IMAGE_REF='gitea.zuev.company/zuev/magistr-frontend@sha256:' - bash ../k8s/deploy.sh apply - ``` - - Скрипт применит ConfigMap, RBAC, Secret mount, probes, OTel и Ingress, поднимет две - реплики backend и дождётся Flyway/readiness. После этого следующие image-only - обновления автоматически разворачиваются успешным workflow после push в `main`. - `workflow_dispatch` остаётся дополнительным способом повторного запуска, но не - требуется для обычной поставки. - -5. Перед открытием публичного доступа проверьте: - - ```bash - kubectl rollout status deployment/backend -n magistr --timeout=300s - kubectl rollout status deployment/frontend -n magistr --timeout=120s - kubectl get pods -n magistr - kubectl logs -n magistr -l app=backend --tail=200 - curl -fsSI https://magistr.zuev.company/ - ``` - - Проверьте в браузере login → reload → refresh → logout для каждого tenant-домена. - Refresh-cookie должна иметь `HttpOnly`, `Secure`, `SameSite=Lax`, path `/api/auth`; - access JWT не должен находиться в Local/Session Storage. До снятия технического окна - смените или отключите все демонстрационные учётные записи из `V1__init.sql`. - -6. После проверки удалите старый `ConfigMap/tenants-config`, отзовите старые DB/OTel - пароли и верните обычный доступ через Caddy. В дальнейшем image-only изменения - автоматически разворачиваются после push в `main`, но любые изменения `../k8s` - по-прежнему нужно применять отдельно через `../k8s/deploy.sh`. - -### Важные ограничения rollback - -- Смена `JWT_SECRET` и сброс БД завершат все старые пользовательские сессии. -- Readiness требует доступности и успешной миграции всех tenant-БД; одна старая или - недоступная БД остановит rollout. -- Автоматический rollback возвращает только два image reference. Он не откатывает БД, - Secret, ConfigMap или RBAC; после применения новой V1 безопаснее исправлять проблему - вперёд либо снова пересоздавать БД под старую версию. -- CI не запускает Testcontainers integration tests: полный PostgreSQL-прогон нужно - повторить вручную, если после зафиксированных 300 успешных тестов код ещё менялся. -- `TRUSTED_PROXY_CIDRS=10.42.0.0/16` соответствует текущей pod-сети K3s. При смене - cluster CIDR его нужно обновить, иначе rate limit увидит неверные клиентские IP. - -## 3. Что могу выполнить я - -По вашей команде я могу подготовить `.env`, запустить и диагностировать локальный Compose, -проверить/исправить код и манифесты, прогнать тесты и dry-run Kubernetes, а при доступном -`kubectl` — проверить состояние кластера. - -Без вас я не могу получить реальные пароли и токены, настроить DNS/сертификаты у внешнего -провайдера или войти в Gitea/Kubernetes, если доступы не предоставлены. Production deploy, -ротацию Secret и удаление БД я не выполняю без отдельного явного разрешения: это меняет -внешнюю систему и может прервать работу либо уничтожить данные.