Merge branch 'department_dev' of ssh://gitea.zuev.company:2222/Zuev/magistr into department_dev

This commit is contained in:
Zuev
2026-03-25 23:53:35 +03:00
22 changed files with 2589 additions and 144 deletions

6
.agents/rules/1.md Normal file
View File

@@ -0,0 +1,6 @@
---
trigger: always_on
glob:
description:
---

View File

@@ -0,0 +1,85 @@
---
name: AutoUpdateDocs
description: Автоматическое обновление документации проекта после изменений в коде
---
# Скилл: Автоматическое обновление документации
## Когда активировать
Этот скилл **ДОЛЖЕН** выполняться автоматически после любых изменений, затрагивающих:
- **Контроллеры** (`backend/src/main/java/com/magistr/app/controller/`) → обновить `docs/API.md`
- **Модели или миграции** (`model/`, `db/migration/`) → обновить `docs/DATABASE.md`
- **Конфигурация тенантов** (`config/tenant/`) → обновить `docs/ARCHITECTURE.md`
- **Бизнес-правила или валидаторы** (`utils/`) → обновить `docs/BUSINESS_LOGIC.md`
- **Frontend** (`frontend/`) → обновить `docs/FRONTEND.md`
- **Docker/Kubernetes** (`compose.yaml`, `Dockerfile`, `../k8s/`) → обновить `docs/INFRASTRUCTURE.md`
- **Code style или структура пакетов** → обновить `docs/DEVELOPMENT.md`
- **Общая структура проекта** → обновить `docs/README.md`
## Карта соответствия «файл → документация»
| Изменённый файл/директория | Файл документации |
|----------------------------|-------------------|
| `controller/*Controller.java` | `docs/API.md` |
| `db/migration/V*__.sql` | `docs/DATABASE.md` |
| `model/*.java` | `docs/DATABASE.md` |
| `dto/*.java` | `docs/API.md` |
| `config/tenant/*.java` | `docs/ARCHITECTURE.md` |
| `utils/*.java` | `docs/BUSINESS_LOGIC.md` |
| `frontend/admin/js/views/*.js` | `docs/FRONTEND.md` |
| `frontend/admin/css/*.css` | `docs/FRONTEND.md` |
| `compose.yaml`, `Dockerfile` | `docs/INFRASTRUCTURE.md` |
| `application.properties` | `docs/ARCHITECTURE.md` |
## Пошаговая инструкция
### 1. Определить затронутые файлы документации
После выполнения задачи пользователя — проверить по таблице выше, какие файлы документации нужно обновить.
### 2. Прочитать текущую документацию
Открыть соответствующий файл из `docs/` и найти секцию, которую нужно обновить.
### 3. Внести точечные изменения
Обновить **только затронутые секции**, не переписывая весь файл. Примеры:
#### Новый контроллер → `docs/API.md`
Добавить новую секцию с описанием эндпоинтов:
- Метод + URL
- Тело запроса (JSON пример)
- Ответ (JSON пример)
- Валидация
#### Новая миграция → `docs/DATABASE.md`
- Добавить новую таблицу в ER-диаграмму (Mermaid)
- Добавить описание таблицы и колонок
- Добавить запись в таблицу «Текущие миграции»
#### Новый view → `docs/FRONTEND.md`
- Добавить в дерево файлов
- Добавить в таблицу «Разделы админ-панели»
### 4. Обновить AGENTS.md (при необходимости)
Если изменения затрагивают:
- Структуру директорий → обновить дерево в `AGENTS.md`
- Критические правила (Flyway, новые ограничения) → обновить секцию «Критические правила»
### 5. Сообщить пользователю
В конце ответа кратко упомянуть, какие файлы документации были обновлены:
> 📝 Обновлена документация: `docs/API.md` (добавлен эндпоинт `POST /api/absences`)
## Правила
1. **Язык:** Вся документация на русском языке
2. **Формат:** Сохранять существующий стиль оформления файла (заголовки, таблицы, примеры кода)
3. **Не удалять:** Не удалять существующие секции без явного запроса пользователя
4. **Mermaid:** При изменении схемы БД — обязательно обновлять ER-диаграмму в `docs/DATABASE.md`
5. **Минимальные правки:** Не переписывать весь файл ради добавления одной строки — использовать точечные изменения
6. **Консистентность:** Если одно и то же понятие упоминается в нескольких файлах `docs/`, обновить все вхождения

29
.agents/skills/SKILL.md Normal file
View File

@@ -0,0 +1,29 @@
Контекст проекта:
Backend: Java 17, Spring Framework. Учитывай возможности этой версии языка и стандарты фреймворка.
Frontend: HTML, CSS, JavaScript.
Правила написания кода и комментариев:
Проверка: Перед написанием кода изучи проект, его структуру и используемые технологии. Не предлагай решения, которые не соответствуют текущей архитектуре или стеку.
Язык: Все комментарии и объяснения должны быть строго на русском языке.
Комментирование кода: Оставляй комментарии, объясняющие, за что отвечает та или иная часть кода. Перед крупными или смысловыми блоками обязательно ставь поясняющие метки (например: ``, /* таблица subjects */, // логика обработки subjects).
Обоснование решений: При написании нового кода кратко и максимально понятно объясняй, почему мы используем именно это решение, а не другое.
Современные подходы: На фронтенде используй самые современные и актуальные подходы (например, Flexbox, CSS Grid, семантические теги).
Правила работы с ошибками (Обучающий режим):
Если ты находишь ошибку в моем коде, не пиши сразу готовый исправленный код.
Дай мне точную подсказку, чтобы я мог сам найти и исправить баг (например: "У тебя не закрыт тег в 15 строке", "Ты забыл поставить аннотацию в контроллере Spring" или "Проверь отступы в таком-то классе"). Моя цель — научиться.
Правила работы с дизайном (UI/UX):
Перед добавлением новых стилей всегда сначала изучай, какие стили уже используются в проекте, чтобы сохранять единообразие.
Если ты видишь, что текущий дизайн откровенно плох, нелогичен или устарел — смело предлагай свои идеи по улучшению (цветовая палитра, отступы, шрифты). Я открыт к предложениям по улучшению визуала.

View File

@@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@@ -0,0 +1,42 @@
---
name: frontend-design
description: Создание выразительных, готовых к продакшену frontend-интерфейсов с высоким качеством дизайна. Используйте этот навык, когда пользователь просит разработать веб-компоненты, страницы, артефакты, постеры или приложения (например, сайты, лендинги, дашборды, React-компоненты, HTML/CSS верстку или когда нужно стилизовать/улучшить любой веб-интерфейс). Генерирует креативный, отточенный код и UI-дизайн, избегая шаблонной эстетики ИИ.
license: Полные условия в LICENSE.txt
---
Этот навык направляет создание выразительных, готовых к продакшену frontend-интерфейсов, которые избегают шаблонной "ИИ-эстетики". Создавайте реально работающий код с исключительным вниманием к эстетическим деталям и творческим решениям.
Пользователь предоставляет требования к фронтенду: компонент, страницу, приложение или интерфейс для разработки. Требования могут включать контекст о цели, аудитории или технических ограничениях.
## Дизайн-мышление
Перед написанием кода поймите контекст и примите СМЕЛОЕ эстетическое направление:
- **Цель**: Какую проблему решает этот интерфейс? Кто им пользуется?
- **Тон**: Выберите крайность: брутальный минимализм, максималистский хаос, ретро-футуризм, органический/природный, люксовый/утонченный, игривый/игрушечный, редакционный/журнальный, брутализм/грубый, арт-деко/геометрический, мягкий/пастельный, индустриальный/утилитарный и т.д. Вариантов очень много. Используйте их для вдохновения, но создайте дизайн, верный выбранному эстетическому направлению.
- **Ограничения**: Технические требования (фреймворк, производительность, доступность).
- **Отличительная черта**: Что делает это НЕЗАБЫВАЕМЫМ? Какую единственную вещь кто-то запомнит?
**КРИТИЧЕСКИ ВАЖНО**: Выберите четкое концептуальное направление и выполните его с точностью. Смелый максимализм и утонченный минимализм — оба работают, ключ кроется в осознанности намерений, а не в интенсивности.
Затем реализуйте рабочий код (HTML/CSS/JS, React, Vue и т.д.), который:
- Готов к продакшену и функционален
- Визуально поразителен и легко запоминается
- Согласован с четкой эстетической точкой зрения
- Тщательно проработан в каждой детали
## Руководство по эстетике фронтенда
Сфокусируйтесь на:
- **Типографика**: Выбирайте шрифты, которые красивы, уникальны и интересны. Избегайте общих шрифтов, таких как Arial и Inter; вместо этого делайте выбор в пользу выразительных, неожиданных и характерных вариантов, которые повышают уровень эстетики фронтенда. Сочетайте акцидентный шрифт (display) с утонченным текстовым (body).
- **Цвет и тема**: Придерживайтесь согласованной эстетики. Используйте CSS-переменные для консистентности. Доминирующие цвета с резкими акцентами работают намного лучше, чем робкие, равномерно распределенные палитры.
- **Анимация (Motion)**: Используйте анимации для эффектов и микро-взаимодействий. Отдавайте предпочтение CSS-решениям для HTML. Используйте библиотеки анимаций для React, если они доступны. Фокусируйтесь на моментах с высоким влиянием: одна хорошо срежиссированная загрузка страницы с каскадным появлением элементов (animation-delay) создает больше восторга, чем множество разрозненных микро-взаимодействий. Используйте триггеры при скролле (scroll-triggering) и состояния наведения (hover), которые удивляют.
- **Пространственная композиция**: Неожиданные макеты. Асимметрия. Перекрытие. Диагональное направление. Элементы, ломающие сетку. Обильное негативное пространство ИЛИ контролируемая плотность элементов.
- **Фоны и визуальные детали**: Создавайте атмосферу и глубину вместо использования скучных сплошных цветов по умолчанию. Добавляйте контекстуальные эффекты и текстуры, соответствующие общей эстетике. Применяйте творческие формы: градиентные сетки, шумовые текстуры, геометрические паттерны, слоистые прозрачности, драматичные тени, декоративные рамки, кастомные курсоры и эффекты зернистости (grain).
НИКОГДА не используйте шаблонную сгенерированную ИИ эстетику: заезженные семейства шрифтов (Inter, Roboto, Arial, системные шрифты), клишированные цветовые схемы (особенно фиолетовые градиенты на белом фоне), предсказуемые макеты и паттерны компонентов, а также типовой скучный дизайн без характера, не учитывающий контекст.
Интерпретируйте творчески и делайте неожиданные выборы, которые кажутся действительно разработанными под данный контекст. Ни один дизайн не должен быть шаблонным ("под копирку"). Варьируйте между светлыми и темными темами, разными шрифтами, различной эстетикой. НИКОГДА не сходитесь к общим выборам (например, Space Grotesk) в разных генерациях кода.
**ВАЖНО**: Сопоставляйте сложность реализации с эстетическим видением. Максималистские дизайны требуют сложного кода с масштабными анимациями и эффектами. Минималистские или утонченные дизайны требуют сдержанности, точности и крайне внимательного отношения к отступам, типографике и тонким деталям. Элегантность исходит из хорошего воплощения видения.
Помните: ИИ способен на выдающуюся творческую работу. Не сдерживайтесь, покажите, что можно создать на самом деле, когда вы мыслите нестандартно и полностью привержены особому видению.

3
.gitignore vendored
View File

@@ -7,8 +7,7 @@ backend/build/
frontend/node_modules/ frontend/node_modules/
frontend/dist/ frontend/dist/
.agents
.idea/ .idea/
.vscode/ .vscode/
*.DS_Store *.DS_Store
GEMINI.md skills-lock.json

170
AGENTS.md
View File

@@ -28,174 +28,60 @@ magistr/
│ ├── admin/ # Интерфейс администратора │ ├── admin/ # Интерфейс администратора
│ ├── teacher/ # Интерфейс преподавателя │ ├── teacher/ # Интерфейс преподавателя
│ └── student/ # Интерфейс студента │ └── student/ # Интерфейс студента
├── docs/ # 📖 Документация проекта
├── compose.yaml # Docker Compose конфигурация ├── compose.yaml # Docker Compose конфигурация
└── .env # Переменные окружения └── .env # Переменные окружения
``` ```
**Внешние зависимости (родительская директория)** **Внешние зависимости (родительская директория)**
На уровень выше расположен `../caddy-proxy/`. Это реверс-прокси, обрабатывающий трафик для `magistr.zuev.company`. Если возникают проблемы с доменом или внешним доступом, проверяйте `Caddyfile` там. На уровень выше расположен конфиг kubernetes `../k8s/`, все файлы сборки на проде расположены там.
так же на уровень выше расположен конфиг kubernetes `../k8s/`, все файлы сборки на проде расположены там.
--- ---
## Команды сборки и запуска ## Быстрый справочник команд
### Docker Compose (основной способ)
Сборка и запуск всех сервисов (backend, frontend, PostgreSQL) выполняется через Docker Compose.
```bash ```bash
# Сборка и запуск всех сервисов # Сборка и запуск
docker compose up -d --build docker compose up -d --build
# Остановка всех сервисов # Полный сброс БД
docker compose down docker compose down -v && docker compose up -d
# Просмотр логов всех сервисов # Логи конкретного сервиса
docker compose logs -f
# Просмотр логов конкретного сервиса
docker compose logs -f backend docker compose logs -f backend
# Пересоздать контейнер базы данных (полный сброс данных и повтор миграций Flyway)
docker compose down -v
docker compose up -d db
``` ```
### Frontend Подробнее — см. [`docs/README.md`](docs/README.md) и [`docs/INFRASTRUCTURE.md`](docs/INFRASTRUCTURE.md).
Статические файлы обслуживаются через Apache (httpd:alpine). Изменения в файлах frontend требуют пересборки контейнера.
--- ---
## Соглашения о коде (Code Style) ## Критические правила для агентов
### Java (Backend) ### Flyway миграции
- **ЗАПРЕЩЕНО** изменять существующие файлы миграций (например, `V1__init.sql`). Это сломает контрольные суммы Flyway.
**Именование:** - Новые миграции: `V{N}__{описание}.sql` в `backend/src/main/resources/db/migration/`
- Классы: PascalCase (например, `LessonsController`, `LessonResponse`) - Подробнее — см. [`docs/DATABASE.md`](docs/DATABASE.md)
- Методы и переменные: camelCase
- Константы: UPPER_SNAKE_CASE
- Пакеты: lowercase (например, `com.magistr.app.controller`)
**Импорты:**
- Группировка: static imports, затем external packages, затем internal
- Используйте wildcard imports для пакетов того же модуля: `import com.magistr.app.model.*;`
- Порядок: java.*, javax.*, external.*, internal.*
**Форматирование:**
- Отступы: 4 пробела (стандарт Java)
- Фигурные скобки: K&R style (открывающая на той же строке)
- Длина строки: до 120 символов
- Всегда используйте фигурные скобки для if/for/while
**Типы и аннотации:**
- Используйте явные типы вместо `var` для возвращаемых значений публичных методов
- Аннотации JPA: `@Entity`, `@Table`, `@Id`, `@GeneratedValue`, `@Column`
- Используйте `@JsonInclude(JsonInclude.Include.NON_NULL)` для DTO
- Для логгирования используйте SLF4J: `LoggerFactory.getLogger(ClassName.class)`
**Обработка ошибок:**
- Возвращайте `ResponseEntity<?>` с соответствующим HTTP статусом
- Логируйте ошибки с полным стектрейсом: `logger.error("msg: {}", e.getMessage(), e)`
- Для валидации используйте отдельные классы-валидаторы (см. `DayAndWeekValidator`)
**Архитектура контроллеров:**
- Используйте constructor injection для зависимостей
- Все endpoints имеют префикс `/api/`
- Возвращайте понятные сообщения об ошибках на русском языке
### Frontend (JavaScript)
**Именование:**
- Файлы: kebab-case (например, `main.js`, `schedule-view.js`)
- Функции и переменные: camelCase
- Константы: UPPER_SNAKE_CASE
**Модули:**
- Используйте ES6 modules с `import`/`export`
- Всегда указывайте расширение при импорте: `import { x } from './api.js';`
**Форматирование:**
- Отступы: 4 пробела
- Используйте template literals вместо конкатенации строк
- Предпочитайте `const` переменные, используйте `let` только при необходимости переприсваивания
**Лучшие практики:**
- Используйте `async/await` для асинхронных операций
- Всегда обрабатывайте ошибки в блоках `catch`
- Используйте деструктуризацию объектов
- Кешируйте DOM-элементы в переменные
---
## Работа с базой данных и мультитенантностью
**Мультитенантность:**
- Приложение поддерживает множество клиентов (университетов). Каждый клиент имеет свою изолированную базу данных PostgreSQL.
- Маршрутизация к нужной БД происходит динамически на основе поддомена (`TenantInterceptor` -> `TenantContext` -> `TenantRoutingDataSource`).
- Список клиентов хранится в Kubernetes `ConfigMap` (`tenants-config`), который монтируется в под бэкенда как `/config/tenants.json`.
- Локально список берётся из файла `backend/tenants.json`.
- При добавлении нового клиента в интерфейсе `DatabaseController` через K8s API обновляет `ConfigMap`. Все реплики бэкенда заметят изменения и в фоне инициализируют новый пул соединений (`TenantConfigWatcher`).
**Миграции схемы (Flyway):**
- Автогенерация Hibernate ОТКЛЮЧЕНА (`ddl-auto=none`). Структура баз данных управляется строго через **Flyway**.
- Все изменения схемы БД вносятся путем создания новых файлов в `backend/src/main/resources/db/migration/` (название строго `V2__add_new_table.sql` и т.д.).
- **ЗАПРЕЩЕНО** изменять существующие файлы миграций (например, `V1__init.sql`), которые уже закоммичены. Это сломает контрольные суммы Flyway.
- Flyway запускается программно при первом обращении к базе тенанта. Чтобы запустить Flyway для уже существующих тенантов (накатить V2), необходимо перезапустить бэкенд: `kubectl rollout restart deployment backend -n magistr`.
- Для локального сброса базы до изначального состояния: `docker compose down -v && docker compose up -d`.
**Сущности и связи:**
- Foreign keys с `ON DELETE CASCADE` для поддержания целостности
- Используйте расширение `pgcrypto` для хеширования паролей (bcrypt)
---
## Функциональные требования к системе (Бизнес-логика)
### 1. Ролевая модель
- **Администратор (Деканат)**: Полный доступ, настройка топологии университета, управление аудиторным фондом, подтверждение переносов, регистрация инцидентов.
- **Преподаватель**: Просмотр своего расписания, подача заявок на перенос, отметка о своём отсутствии.
- **Студент**: Только просмотр расписания (Read-only).
### 2. Управление ресурсами и топология
- **Управление аудиториями**:
- Указание вместимости.
- Привязка доступного оборудования (через сущность Equipments: Проектор, ПК, Лаборатория).
- Установка статуса "Не доступно" (блокирует назначение пар в этот период).
- **Управление группами**:
- Управление списком студентов (и возможность деления на подгруппы).
- **Управление дисциплинами**:
- Создание предметов и привязка их к преподавателям (какие дисциплины имеет право вести конкретный преподаватель).
### 3. Логика расписания
- **Сетка**: 7 фиксированных слотов по 1.5 часа (08:00 - 09:30, и т.д.) + поддержка кастомного времени.
- **Проверка конфликтов**:
- *Критический конфликт*: Преподаватель не может находиться в двух разных аудиториях одновременно.
- *Уточнение по преподавателям*: Преподаватель может иметь несколько пар одновременно (для разных групп), только если они проходят в одной и той же аудитории (потоковая лекция).
- **Потоковые занятия**:
- Возможность назначить одну лекцию сразу нескольким группам (технически — несколько записей в БД или одна запись со списком групп).
- Проверка вместимости: вместимость аудитории должна покрывать суммарную численность всех групп, находящихся в этой аудитории в данный слот.
### 4. Управление инцидентами (Инклюзия отсутствия)
- **Отсутствие (Sickness/Business Trip)**: Регистрация отсутствия преподавателя (с указанием причины и периода дат).
- **Обнаружение коллизий**: Автоматическая подсветка конфликтующих пар в расписании (Red Zone).
- **Система разрешения конфликтов (Resolution Wizard)**:
- Предложение подходящей замены преподавателя на этот слот.
- Предложение переноса занятия на другое время или в другую аудиторию.
---
## Языковые требования
### Языковые требования
- **Все ответы и комментарии на русском языке** - **Все ответы и комментарии на русском языке**
- Сообщения об ошибках и логи на русском - Сообщения об ошибках и логи на русском
- Пользовательский интерфейс на русском - Пользовательский интерфейс на русском
--- ---
## Существующие правила проекта ## Подробная документация
См. `.agent/rules/main.md` и `.agent/rules/database_schema.md` для полного контекста о функциональных требованиях и схеме БД. Полная документация проекта находится в папке `docs/`:
| Документ | Содержание |
|----------|-----------|
| [`docs/README.md`](docs/README.md) | Обзор проекта, стек технологий, быстрый старт |
| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | Архитектура системы, мультитенантность, аутентификация |
| [`docs/BUSINESS_LOGIC.md`](docs/BUSINESS_LOGIC.md) | Бизнес-логика, ролевая модель, правила расписания |
| [`docs/DATABASE.md`](docs/DATABASE.md) | Схема БД (ER-диаграмма), описание всех таблиц, Flyway |
| [`docs/API.md`](docs/API.md) | REST API эндпоинты с примерами запросов и ответов |
| [`docs/INFRASTRUCTURE.md`](docs/INFRASTRUCTURE.md) | Docker, Kubernetes, CI/CD, мониторинг (SigNoz) |
| [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) | Code Style, соглашения, пошаговое создание нового эндпоинта |
| [`docs/FRONTEND.md`](docs/FRONTEND.md) | Frontend архитектура, SPA-маршрутизация, CSS, адаптивность |
| [`docs/LOGGING.md`](docs/LOGGING.md) | Логирование: SLF4J + Logback, MDC, OpenTelemetry → SigNoz |

420
docs/API.md Normal file
View File

@@ -0,0 +1,420 @@
# 🔌 REST API
Все эндпоинты имеют префикс `/api/`. Ответы возвращаются в формате JSON.
---
## Аутентификация
### `POST /api/auth/login`
Вход в систему.
**Тело запроса:**
```json
{
"username": "admin",
"password": "admin"
}
```
**Успешный ответ (200):**
```json
{
"success": true,
"message": "OK",
"token": "550e8400-e29b-41d4-a716-446655440000",
"role": "ADMIN",
"redirect": "/admin/"
}
```
**Ошибка (401):**
```json
{
"success": false,
"message": "Неверное имя пользователя или пароль",
"token": null,
"role": null,
"redirect": null
}
```
> После получения токена клиент должен передавать его в заголовке: `Authorization: Bearer <token>`
---
## Пользователи
### `GET /api/users`
Список всех пользователей.
**Ответ:**
```json
[
{ "id": 1, "username": "admin", "role": "ADMIN" },
{ "id": 2, "username": "Тестовый преподаватель", "role": "TEACHER" }
]
```
### `GET /api/users/teachers`
Список только преподавателей (роль `TEACHER`).
### `POST /api/users`
Создание пользователя.
**Тело запроса:**
```json
{
"username": "Новый преподаватель",
"password": "password123",
"role": "TEACHER"
}
```
**Валидация:**
- `username` — обязателен
- `password` — минимум 4 символа
- `role``ADMIN`, `TEACHER` или `STUDENT`
### `DELETE /api/users/{id}`
Удаление пользователя.
---
## Расписание (Lessons)
### `GET /api/users/lessons`
Список всех занятий с разрешёнными именами (преподаватель, группа, дисциплина, аудитория).
**Ответ:**
```json
[
{
"id": 1,
"teacherName": "Тестовый преподаватель",
"groupName": "ИВТ-21-1",
"classroomName": "101 Ленинская",
"educationFormName": "Бакалавриат",
"subjectName": "Высшая математика",
"typeLesson": "Лекция",
"lessonFormat": "Очно",
"day": "Понедельник",
"week": "Верхняя",
"time": "11:40 - 13:10"
}
]
```
### `GET /api/users/lessons/{teacherId}`
Занятия конкретного преподавателя.
### `POST /api/users/lessons/create`
Создание занятия.
**Тело запроса:**
```json
{
"teacherId": 2,
"groupId": 1,
"subjectId": 1,
"lessonFormat": "Очно",
"typeLesson": "Лекция",
"classroomId": 1,
"day": "Понедельник",
"week": "Верхняя",
"time": "11:40 - 13:10"
}
```
**Валидация:**
| Поле | Правило |
|------|---------|
| `teacherId` | Обязателен, ≠ 0 |
| `groupId` | Обязателен, ≠ 0 |
| `subjectId` | Обязателен, ≠ 0 |
| `lessonFormat` | `Очно` или `Онлайн` |
| `typeLesson` | `Лекция`, `Практическая работа`, `Лабораторная работа` |
| `classroomId` | Обязателен, ≠ 0 |
| `day` | Пн–Сб (на русском) |
| `week` | `Верхняя`, `Нижняя`, `Обе` |
| `time` | Обязателен |
### `PUT /api/users/lessons/update/{lessonId}`
Обновление занятия. Поддерживает partial update — передаются только изменённые поля.
### `DELETE /api/users/lessons/delete/{lessonId}`
Удаление занятия.
### `GET /api/users/lessons/ping`
Проверка доступности контроллера. Возвращает строку `pong`.
---
## Группы
### `GET /api/groups`
Список всех групп.
**Ответ:**
```json
[
{
"id": 1,
"name": "ИВТ-21-1",
"groupSize": 25,
"educationFormId": 1,
"educationFormName": "Бакалавриат"
}
]
```
### `POST /api/groups`
Создание группы.
```json
{
"name": "ИБ-31м",
"groupSize": 20,
"educationFormId": 2
}
```
### `DELETE /api/groups/{id}`
Удаление группы.
---
## Аудитории
### `GET /api/classrooms`
Список аудиторий с привязанным оборудованием.
**Ответ:**
```json
[
{
"id": 1,
"name": "101 Ленинская",
"capacity": 120,
"isAvailable": true,
"equipments": [
{ "id": 1, "name": "Проектор" },
{ "id": 4, "name": "Интерактивная доска" }
]
}
]
```
### `POST /api/classrooms`
Создание аудитории.
```json
{
"name": "404 Лаборатория",
"capacity": 30,
"isAvailable": true,
"equipmentIds": [1, 2, 3]
}
```
### `PUT /api/classrooms/{id}`
Обновление аудитории (partial update).
### `DELETE /api/classrooms/{id}`
Удаление аудитории.
---
## Дисциплины
### `GET /api/subjects`
Список всех дисциплин.
### `POST /api/subjects`
```json
{ "name": "Физика" }
```
### `DELETE /api/subjects/{id}`
Удаление дисциплины.
---
## Оборудование
### `GET /api/equipments`
Список всего оборудования.
### `POST /api/equipments`
```json
{ "name": "3D-принтер" }
```
### `DELETE /api/equipments/{id}`
Удаление оборудования.
---
## Формы обучения
### `GET /api/education-forms`
Список форм обучения.
**Ответ:**
```json
[
{ "id": 1, "name": "Бакалавриат" },
{ "id": 2, "name": "Магистратура" }
]
```
### `POST /api/education-forms`
```json
{ "name": "Аспирантура" }
```
### `DELETE /api/education-forms/{id}`
Удаление формы обучения. **Невозможно**, если к ней привязаны группы.
---
## Привязка «Преподаватель ↔ Дисциплина»
### `GET /api/teacher-subjects`
Список всех привязок.
**Ответ:**
```json
[
{
"userId": 2,
"userName": "Тестовый преподаватель",
"subjectId": 1,
"subjectName": "Высшая математика"
}
]
```
### `POST /api/teacher-subjects`
```json
{
"userId": 2,
"subjectId": 3
}
```
### `DELETE /api/teacher-subjects`
```json
{
"userId": 2,
"subjectId": 3
}
```
---
## Управление тенантами (Базы данных)
### `GET /api/database/status`
Статус текущего подключения (определяется по домену запроса).
**Ответ:**
```json
{
"tenant": "default",
"connected": true,
"configured": true,
"name": "Default",
"url": "jdbc:postgresql://db:5432/app_db"
}
```
### `GET /api/database/tenants`
Список всех тенантов.
### `POST /api/database/tenants`
Добавление нового тенанта.
```json
{
"name": "СВФУ",
"domain": "swsu",
"url": "jdbc:postgresql://db-host:5432/swsu_db",
"username": "dbuser",
"password": "dbpass"
}
```
**Логика:**
1. Создаёт HikariCP пул для нового тенанта
2. Запускает Flyway миграции на его БД
3. Обновляет Kubernetes ConfigMap
### `DELETE /api/database/tenants/{domain}`
Удаление тенанта.
### `POST /api/database/test`
Тест подключения к произвольной БД (без регистрации тенанта).
```json
{
"url": "jdbc:postgresql://host:5432/testdb",
"username": "user",
"password": "pass"
}
```
**Ответ:**
```json
{
"success": true,
"message": "Подключение успешно!"
}
```
---
## Коды ответов
| Код | Описание |
|-----|----------|
| `200` | Успех |
| `400` | Ошибка валидации (с `message` в теле) |
| `401` | Неверные учётные данные |
| `404` | Ресурс / тенант не найден |
| `500` | Внутренняя ошибка сервера |

142
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,142 @@
# 🏗 Архитектура системы
## Общая схема
```mermaid
graph TD
Client["🌐 Браузер"] -->|HTTPS| Caddy["Caddy Proxy"]
Caddy -->|:80| Frontend["Frontend<br/>(Apache httpd:alpine)"]
Caddy -->|/api/*| Backend["Backend<br/>(Spring Boot 3.2.5)"]
Backend --> TenantRouter{"TenantRoutingDataSource"}
TenantRouter -->|swsu.zuev.company| DB1["PostgreSQL<br/>swsu_db"]
TenantRouter -->|mgu.zuev.company| DB2["PostgreSQL<br/>mgu_db"]
TenantRouter -->|...| DBn["PostgreSQL<br/>tenant_n_db"]
Backend -->|Метрики, Логи, Трейсы| OTel["OpenTelemetry Collector"]
OTel --> SigNoz["SigNoz"]
```
## Компоненты
### Frontend (Apache httpd:alpine)
- **Тип:** Статические файлы (HTML/CSS/JS)
- **Контейнер:** `httpd:alpine` — лёгкий Apache HTTP Server
- **Порт:** 80
- **Содержание:** Три изолированных интерфейса — `admin/`, `teacher/`, `student/`
- **JS-модули:** Vanilla JavaScript с ES6 Modules (`import`/`export`)
### Backend (Spring Boot 3.2.5)
- **Тип:** REST API сервер
- **Язык:** Java 17
- **Порт:** 8080 (внутренний)
- **ORM:** Hibernate (JPA), `ddl-auto=none`
- **Миграции:** Flyway (программный запуск при подключении тенанта)
- **Аутентификация:** bcrypt (через `BCryptPasswordEncoder`), UUID-токены
### PostgreSQL
- **Версия:** `postgres:alpine3.23`
- **Локально:** Одна БД `app_db` (тенант `default`)
- **Продакшн:** Множество БД, по одной на каждый университет (тенант)
### Caddy (реверс-прокси)
- **Расположение:** `../caddy-proxy/`
- **Назначение:** TLS-терминация, маршрутизация запросов к backend/frontend
- **Домен:** `*.zuev.company`
---
## Мультитенантная архитектура
Ключевая особенность системы — изоляция данных каждого университета в отдельной БД PostgreSQL.
### Принцип работы
```mermaid
sequenceDiagram
participant Browser as Браузер
participant Interceptor as TenantInterceptor
participant Context as TenantContext
participant Router as TenantRoutingDataSource
participant DB as PostgreSQL
Browser->>Interceptor: GET /api/users<br/>Host: swsu.zuev.company
Interceptor->>Interceptor: resolveTenant("swsu.zuev.company") → "swsu"
Interceptor->>Context: setCurrentTenant("swsu")
Note over Interceptor: Проверка: hasTenant("swsu")?
Interceptor-->>Browser: 404 если тенант не найден
Note over Context,Router: Обработка запроса контроллером
Router->>Router: determineCurrentLookupKey() → "swsu"
Router->>DB: SQL запрос к swsu_db
DB-->>Browser: Ответ с данными
```
### Ключевые классы
| Класс | Назначение |
|-------|-----------|
| `TenantInterceptor` | Извлекает поддомен из заголовка `Host` и определяет тенант |
| `TenantContext` | `ThreadLocal`-хранилище имени текущего тенанта |
| `TenantRoutingDataSource` | Наследует `AbstractRoutingDataSource`, маршрутизирует запросы к нужной БД |
| `TenantDataSourceConfig` | Загружает конфигурацию тенантов из JSON-файла, создаёт HikariCP пулы |
| `TenantConfigWatcher` | Периодически (каждые 30 сек) перечитывает `tenants.json`, синхронизирует тенантов |
| `ConfigMapUpdater` | Обновляет Kubernetes ConfigMap при добавлении/удалении тенанта через API |
| `TenantConfig` | POJO с параметрами тенанта: `name`, `domain`, `url`, `username`, `password` |
### Определение тенанта
Логика определения тенанта по заголовку `Host`:
| Host | Результат |
|------|----------|
| `swsu.zuev.company` | `swsu` |
| `mgu.zuev.company` | `mgu` |
| `localhost` | `default` |
| `localhost:8080` | `default` |
| `192.168.1.1` | `default` |
### Конфигурация тенантов
Список тенантов хранится в JSON-файле:
- **Локально:** `backend/tenants.json`
- **Продакшн:** Kubernetes ConfigMap `tenants-config`, монтируется в `/config/tenants.json`
Формат:
```json
[
{
"name": "ЮЗГУ",
"domain": "swsu",
"url": "jdbc:postgresql://db-host:5432/swsu_db",
"username": "dbuser",
"password": "dbpass"
}
]
```
### Жизненный цикл тенанта
1. **Добавление через API:** `POST /api/database/tenants` → создаёт HikariCP пул → запускает Flyway миграции → обновляет ConfigMap
2. **Синхронизация подов:** `TenantConfigWatcher` каждые 30 сек проверяет `tenants.json` → добавляет новые / удаляет отсутствующие тенанты
3. **Удаление:** `DELETE /api/database/tenants/{domain}` → закрывает пул → обновляет ConfigMap
### Fallback при отсутствии тенантов
Если при запуске нет ни одного настроенного тенанта:
1. Проверяется наличие `spring.datasource.url` → создаётся тенант `default`
2. Если datasource тоже нет → создаётся H2 in-memory заглушка для инициализации Spring JPA
---
## Аутентификация
Система использует **простую модель аутентификации** без JWT или Spring Security фильтров:
1. Клиент отправляет `POST /api/auth/login` с `username` и `password`
2. Backend проверяет пароль через `BCryptPasswordEncoder`
3. При успехе возвращается:
- UUID-токен (для заголовка `Authorization: Bearer`)
- Роль пользователя (`ADMIN`, `TEACHER`, `STUDENT`)
- Redirect URL (`/admin/`, `/teacher/`, `/student/`)
4. Токен хранится в `localStorage` на клиенте

149
docs/BUSINESS_LOGIC.md Normal file
View File

@@ -0,0 +1,149 @@
# 📋 Бизнес-логика
## Ролевая модель
Система поддерживает три роли пользователей:
| Роль | Enum | Возможности |
|------|------|------------|
| **Администратор** (Деканат) | `ADMIN` | Полный доступ: CRUD пользователей, групп, аудиторий, дисциплин, расписания. Управление тенантами (БД). |
| **Преподаватель** | `TEACHER` | Просмотр своего расписания. В перспективе — подача заявок на перенос. |
| **Студент** | `STUDENT` | Только просмотр расписания (Read-only). |
После авторизации пользователь перенаправляется на свой интерфейс:
- `ADMIN``/admin/`
- `TEACHER``/teacher/`
- `STUDENT``/student/`
---
## Управление ресурсами
### Кафедры (Departments)
Организационные единицы университета. К кафедре привязываются пользователи, группы и дисциплины.
- Имеют уникальный числовой `code`
- Предзаполнены: «Кафедра ИБ», «Кафедра ВТ», «Кафедра КТ»
### Специальности (Specialties)
Учебные направления с кодом по ФГОС.
- Примеры: «Информационная безопасность» (10.03.01), «Программная инженерия» (09.03.04)
### Формы обучения (Education Forms)
Уровни/формы обучения для привязки к группам.
- Предзаполнены: Бакалавриат, Магистратура, Специалитет
- Нельзя удалить форму обучения, если к ней привязаны группы
### Учебные группы (Student Groups)
- **Поля:** Название (уникальное), численность, форма обучения, кафедра, курс (16)
- **Подгруппы:** Возможно деление группы на подгруппы (таблица `subgroups`)
### Аудитории (Classrooms)
- **Поля:** Название (уникальное), вместимость (> 0), корпус, этаж, доступность
- **Оборудование:** К каждой аудитории привязывается список оборудования (Many-to-Many) с указанием количества
- **Статус:** Флаг `is_available` для блокирования назначения пар
### Оборудование (Equipments)
Каталог оборудования для привязки к аудиториям.
- Предзаполнены: Проектор, ПК, Лаборатория, Интерактивная доска, Документ-камера, Аудиосистема
- Уникальность по названию
### Дисциплины (Subjects)
- **Поля:** Название (уникальное), код, кафедра, описание
- Привязка преподавателей через `teacher_subjects` (Many-to-Many)
---
## Логика расписания
### Сущность «Занятие» (Lesson)
Каждая запись в расписании содержит:
| Поле | Описание | Пример |
|------|----------|--------|
| `teacher_id` | Преподаватель | 2 |
| `group_id` | Учебная группа | 1 |
| `subject_id` | Дисциплина | 3 |
| `lesson_format` | Формат проведения | `Очно`, `Онлайн` |
| `type_lesson` | Тип занятия | `Лекция`, `Практическая работа`, `Лабораторная работа` |
| `classroom_id` | Аудитория | 1 |
| `day` | День недели | `Понедельник` ... `Суббота` |
| `week` | Чётность недели | `Верхняя`, `Нижняя`, `Обе` |
| `time` | Временной слот | `8:00 - 9:30` |
### Временны́е слоты
Система использует 7 фиксированных слотов по 90 минут:
| № | Время |
|---|-------|
| 1 | 08:00 09:30 |
| 2 | 09:40 11:10 |
| 3 | 11:40 13:10 |
| 4 | 13:30 15:00 |
| 5 | 15:00 16:30 |
| 6 | 16:40 18:10 |
| 7 | 18:30 20:00 |
### Валидация при создании/обновлении
- **Дни:** только `Понедельник` `Суббота` (`DayAndWeekValidator`)
- **Недели:** только `Верхняя`, `Нижняя`, `Обе`
- **Формат:** только `Очно`, `Онлайн` (`TypeAndFormatLessonValidator`)
- **Тип:** только `Лекция`, `Практическая работа`, `Лабораторная работа`
- Все ID (преподаватель, группа, дисциплина, аудитория) обязательны и не могут быть 0
### Данные к составлению расписания (Schedule Data)
Таблица `schedule_data` хранит **плановую нагрузку** для составления расписания:
| Поле | Описание |
|------|----------|
| `department_id` | Кафедра |
| `semester` | Номер семестра |
| `group_id` | Учебная группа |
| `subjects_id` | Дисциплина |
| `lesson_type_id` | Тип занятия |
| `number_of_hours` | Количество часов |
| `is_division` | Деление на подгруппы |
| `teacher_id` | Преподаватель |
| `semester_type` | Тип семестра (Весенний / Осенний) |
| `period` | Учебный год (напр. `2024/2025`) |
---
## Привязка преподаватель ↔ дисциплина
Связь Many-to-Many через таблицу `teacher_subjects`:
- Указывается, какие дисциплины может вести конкретный преподаватель
- Дополнительные поля: `qualification_level`, `experience_years`
Дополнительная связь через `teacher_lesson_types`:
- Определяет, какие **типы занятий** (лекция, практика, лаба) может вести преподаватель по конкретной дисциплине
---
## Бизнес-правила (планируемые)
> **Примечание:** Следующие правила описаны в требованиях, но пока не полностью реализованы в коде.
### Проверка конфликтов
- **Критический конфликт:** Преподаватель не может одновременно находиться в двух разных аудиториях
- **Исключение:** Преподаватель может вести несколько пар одновременно (потоковая лекция), если все группы в одной аудитории
- **Вместимость:** Суммарная численность всех групп в слоте не должна превышать вместимость аудитории
### Управление инцидентами
- Регистрация отсутствия преподавателя (болезнь, командировка) с указанием периода
- Автоматическая подсветка конфликтующих пар (Red Zone)
- Resolution Wizard: предложение замены преподавателя или переноса занятия

362
docs/DATABASE.md Normal file
View File

@@ -0,0 +1,362 @@
# 🗄 База данных
## Общая информация
- **СУБД:** PostgreSQL (локально `postgres:alpine3.23`, продакшн — managed PostgreSQL)
- **Управление схемой:** Flyway (программный запуск)
- **Hibernate DDL:** Отключён (`ddl-auto=none`)
- **Расширения:** `pgcrypto` (bcrypt-хеширование паролей)
- **Мультитенантность:** Каждый тенант = отдельная БД
---
## ER-диаграмма
```mermaid
erDiagram
departments {
BIGSERIAL id PK
VARCHAR name
BIGINT code UK
}
specialties {
BIGSERIAL id PK
VARCHAR name
VARCHAR specialty_code
}
users {
BIGSERIAL id PK
VARCHAR username UK
VARCHAR password
VARCHAR role
VARCHAR full_name
VARCHAR job_title
BIGINT department_id FK
TIMESTAMP created_at
TIMESTAMP updated_at
}
education_forms {
BIGSERIAL id PK
VARCHAR name UK
TEXT description
TIMESTAMP created_at
}
student_groups {
BIGSERIAL id PK
VARCHAR name UK
BIGINT group_size
BIGINT education_form_id FK
BIGINT department_id FK
INT course
TIMESTAMP created_at
}
subgroups {
BIGSERIAL id PK
BIGINT group_id FK
VARCHAR name
INT student_capacity
}
subjects {
BIGSERIAL id PK
VARCHAR name UK
VARCHAR code
BIGINT department_id FK
TEXT description
TIMESTAMP created_at
}
lesson_types {
BIGSERIAL id PK
VARCHAR name UK
VARCHAR color_code
INT duration_minutes
}
equipments {
BIGSERIAL id PK
VARCHAR name UK
TEXT description
VARCHAR inventory_number
}
classrooms {
BIGSERIAL id PK
VARCHAR name UK
INT capacity
VARCHAR building
INT floor
BOOLEAN is_available
TEXT description
TIMESTAMP created_at
}
classroom_equipments {
BIGINT classroom_id FK,PK
BIGINT equipment_id FK,PK
INT quantity
TEXT notes
}
teacher_subjects {
BIGINT user_id FK,PK
BIGINT subject_id FK,PK
VARCHAR qualification_level
INT experience_years
}
teacher_lesson_types {
BIGINT user_id FK,PK
BIGINT subject_id FK,PK
BIGINT lesson_type_id FK,PK
}
lessons {
BIGSERIAL id PK
BIGINT teacher_id FK
BIGINT group_id FK
BIGINT subject_id FK
VARCHAR lesson_format
VARCHAR type_lesson
BIGINT classroom_id FK
VARCHAR day
VARCHAR week
VARCHAR time
}
schedule_data {
BIGSERIAL id PK
BIGINT department_id FK
INT semester
BIGINT group_id FK
BIGINT subjects_id FK
BIGINT lesson_type_id FK
INT number_of_hours
BOOLEAN is_division
BIGINT teacher_id FK
VARCHAR semester_type
VARCHAR period
}
departments ||--o{ users : "department_id"
departments ||--o{ student_groups : "department_id"
departments ||--o{ subjects : "department_id"
departments ||--o{ schedule_data : "department_id"
education_forms ||--o{ student_groups : "education_form_id"
student_groups ||--o{ subgroups : "group_id"
student_groups ||--o{ lessons : "group_id"
student_groups ||--o{ schedule_data : "group_id"
users ||--o{ lessons : "teacher_id"
users ||--o{ teacher_subjects : "user_id"
users ||--o{ teacher_lesson_types : "user_id"
users ||--o{ schedule_data : "teacher_id"
subjects ||--o{ lessons : "subject_id"
subjects ||--o{ teacher_subjects : "subject_id"
subjects ||--o{ teacher_lesson_types : "subject_id"
subjects ||--o{ schedule_data : "subjects_id"
lesson_types ||--o{ teacher_lesson_types : "lesson_type_id"
lesson_types ||--o{ schedule_data : "lesson_type_id"
classrooms ||--o{ lessons : "classroom_id"
classrooms ||--o{ classroom_equipments : "classroom_id"
equipments ||--o{ classroom_equipments : "equipment_id"
```
---
## Описание таблиц
### Справочники высшего уровня
#### `departments` — Кафедры
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID кафедры |
| `name` | VARCHAR(255) | Название кафедры |
| `code` | BIGINT UNIQUE | Код кафедры |
#### `specialties` — Специальности
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID специальности |
| `name` | VARCHAR(255) | Название специальности |
| `specialty_code` | VARCHAR(255) | Код ФГОС (напр. `10.03.01`) |
### Пользователи
#### `users` — Пользователи системы
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID пользователя |
| `username` | VARCHAR(50) UNIQUE | Логин |
| `password` | VARCHAR(255) | bcrypt-хеш пароля |
| `role` | VARCHAR(20) | `ADMIN`, `TEACHER`, `STUDENT` |
| `full_name` | VARCHAR(255) | ФИО |
| `job_title` | VARCHAR(255) | Должность |
| `department_id` | BIGINT FK → departments | Кафедра |
| `created_at` | TIMESTAMP | Дата создания |
| `updated_at` | TIMESTAMP | Дата обновления (авто-триггер) |
> **Триггер:** `update_users_updated_at` автоматически обновляет `updated_at` при любом `UPDATE`.
### Учебный процесс
#### `education_forms` — Формы обучения
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `name` | VARCHAR(100) UNIQUE | Название (Бакалавриат, Магистратура, Специалитет) |
| `description` | TEXT | Описание |
#### `student_groups` — Учебные группы
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `name` | VARCHAR(100) UNIQUE | Название группы (напр. `ИВТ-21-1`) |
| `group_size` | BIGINT | Количество студентов |
| `education_form_id` | BIGINT FK → education_forms | Форма обучения |
| `department_id` | BIGINT FK → departments | Кафедра |
| `course` | INT CHECK(16) | Курс |
#### `subgroups` — Подгруппы
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `group_id` | BIGINT FK → student_groups (CASCADE) | Родительская группа |
| `name` | VARCHAR(100) | Название подгруппы |
| `student_capacity` | INT | Количество студентов |
#### `subjects` — Дисциплины
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `name` | VARCHAR(200) UNIQUE | Название |
| `code` | VARCHAR(20) | Код предмета |
| `department_id` | BIGINT FK → departments | Кафедра |
| `description` | TEXT | Описание |
### Аудиторный фонд
#### `classrooms` — Аудитории
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `name` | VARCHAR(50) UNIQUE | Название (напр. `101 Ленинская`) |
| `capacity` | INT CHECK(> 0) | Вместимость |
| `building` | VARCHAR(50) | Корпус |
| `floor` | INT | Этаж |
| `is_available` | BOOLEAN | Доступна для назначения пар |
| `description` | TEXT | Описание |
#### `equipments` — Оборудование
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `name` | VARCHAR(50) UNIQUE | Название |
| `description` | TEXT | Описание |
| `inventory_number` | VARCHAR(50) | Инвентарный номер |
#### `classroom_equipments` — Привязка оборудования к аудиториям
| Колонка | Тип | Описание |
|---------|-----|----------|
| `classroom_id` | BIGINT PK, FK → classrooms (CASCADE) | Аудитория |
| `equipment_id` | BIGINT PK, FK → equipments (CASCADE) | Оборудование |
| `quantity` | INT CHECK(> 0) | Количество единиц |
| `notes` | TEXT | Примечания |
### Расписание
#### `lessons` — Основное расписание занятий
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `teacher_id` | BIGINT FK → users | Преподаватель |
| `group_id` | BIGINT FK → student_groups | Группа |
| `subject_id` | BIGINT FK → subjects | Дисциплина |
| `lesson_format` | VARCHAR(255) | `Очно` / `Онлайн` |
| `type_lesson` | VARCHAR(255) | `Лекция` / `Практическая работа` / `Лабораторная работа` |
| `classroom_id` | BIGINT FK → classrooms | Аудитория |
| `day` | VARCHAR(255) | День недели |
| `week` | VARCHAR(255) | `Верхняя` / `Нижняя` / `Обе` |
| `time` | VARCHAR(255) | Временной слот |
#### `lesson_types` — Типы занятий (справочник)
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `name` | VARCHAR(50) UNIQUE | Название типа |
| `color_code` | VARCHAR(7) | HEX-цвет для UI (напр. `#FF6B6B`) |
| `duration_minutes` | INT | Длительность (по умолчанию 90) |
### Связи «Преподаватель ↔ Дисциплина»
#### `teacher_subjects` — Квалификация преподавателей
| Колонка | Тип | Описание |
|---------|-----|----------|
| `user_id` | BIGINT PK, FK → users (CASCADE) | Преподаватель |
| `subject_id` | BIGINT PK, FK → subjects (CASCADE) | Дисциплина |
| `qualification_level` | VARCHAR(50) | Уровень квалификации |
| `experience_years` | INT | Стаж |
#### `teacher_lesson_types` — Типы занятий преподавателя
| Колонка | Тип | Описание |
|---------|-----|----------|
| `user_id` | BIGINT PK, FK → users (CASCADE) | Преподаватель |
| `subject_id` | BIGINT PK, FK → subjects (CASCADE) | Дисциплина |
| `lesson_type_id` | BIGINT PK, FK → lesson_types (CASCADE) | Тип занятия |
#### `schedule_data` — Данные к составлению расписания
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `department_id` | BIGINT FK → departments | Кафедра |
| `semester` | INT | Номер семестра |
| `group_id` | BIGINT FK → student_groups | Группа |
| `subjects_id` | BIGINT FK → subjects | Дисциплина |
| `lesson_type_id` | BIGINT FK → lesson_types | Тип занятия |
| `number_of_hours` | INT | Количество часов |
| `is_division` | BOOLEAN | Деление на подгруппы |
| `teacher_id` | BIGINT FK → users | Преподаватель |
| `semester_type` | VARCHAR(255) | Весенний / Осенний |
| `period` | VARCHAR(255) | Учебный год |
---
## Flyway миграции
### Правила работы
1. Все миграции находятся в `backend/src/main/resources/db/migration/`
2. Формат имени: `V{номер}__{описание}.sql` (напр. `V1__init.sql`, `V2__add_departments.sql`)
3. **ЗАПРЕЩЕНО** изменять уже закоммиченные файлы миграций — это сломает контрольные суммы Flyway
4. Flyway запускается **программно** при первом обращении к БД тенанта (`TenantConfigWatcher.initDatabaseForTenant()`)
5. Настройка `baselineOnMigrate=true` — если в БД уже есть данные, Flyway начнёт с baseline
### Текущие миграции
| Файл | Описание |
|------|----------|
| `V1__init.sql` | Инициализация: все таблицы, тестовые данные, триггеры, комментарии |
### Накатывание на существующих тенантов
Для применения новой миграции к уже существующим тенантам необходимо перезапустить backend:
```bash
# Kubernetes
kubectl rollout restart deployment backend -n magistr
# Docker Compose (локально)
docker compose restart backend
```
### Полный сброс БД (локально)
```bash
docker compose down -v # Удаляет volumes (данные)
docker compose up -d # Пересоздаёт БД с нуля
```

275
docs/DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,275 @@
# 🛠 Руководство для разработчиков
## Локальный запуск
### Предварительные требования
- Docker и Docker Compose
- Git
- (Опционально) Java 17 + Maven 3.9+ для запуска backend вне Docker
### Первый запуск
```bash
# Создать Docker-сеть
docker network create proxy
# Собрать и запустить
docker compose up -d --build
# Убедиться, что всё работает
docker compose logs -f
```
Приложение доступно: **http://localhost:80**
### Пересборка после изменений
```bash
# Пересобрать только backend
docker compose up -d --build backend
# Пересобрать только frontend
docker compose up -d --build frontend
```
### Полный сброс данных
```bash
docker compose down -v # Удаляет БД
docker compose up -d # Пересоздаёт с нуля
```
---
## Соглашения о коде
### Java (Backend)
#### Именование
| Категория | Стиль | Пример |
|-----------|-------|--------|
| Классы | PascalCase | `LessonsController`, `LessonResponse` |
| Методы и переменные | camelCase | `getAllLessons()`, `teacherId` |
| Константы | UPPER_SNAKE_CASE | `ROLE_REDIRECTS` |
| Пакеты | lowercase | `com.magistr.app.controller` |
#### Архитектурные правила
- **Constructor Injection** — все зависимости через конструктор (не `@Autowired` на поля)
- **Controller → Repository** — контроллеры работают напрямую с репозиториями (без слоя service)
- **Префикс `/api/`** — все REST-эндпоинты
- **`ResponseEntity<?>`** — все мутирующие методы возвращают `ResponseEntity` с HTTP-статусом
- **Сообщения на русском** — все ошибки и уведомления на русском языке
#### Логирование
Используйте SLF4J:
```java
private static final Logger logger = LoggerFactory.getLogger(MyController.class);
// Информационные сообщения
logger.info("Запрос на получение всех занятий");
// Ошибки с полным стектрейсом
logger.error("Ошибка при сохранении: {}", e.getMessage(), e);
```
#### Валидация
- Для сложных правил — отдельные классы-валидаторы (`DayAndWeekValidator`, `TypeAndFormatLessonValidator`)
- Для простых — inline-проверки в контроллере с `ResponseEntity.badRequest()`
#### Импорты
```java
// 1. Static imports
import static org.junit.Assert.*;
// 2. Java/Jakarta
import java.util.*;
import jakarta.persistence.*;
// 3. External libraries
import org.springframework.web.bind.annotation.*;
import com.fasterxml.jackson.databind.ObjectMapper;
// 4. Internal packages (wildcard для того же модуля)
import com.magistr.app.model.*;
import com.magistr.app.repository.*;
```
#### Форматирование
- **Отступы:** 4 пробела
- **Скобки:** K&R style (открывающая на той же строке)
- **Длина строки:** до 120 символов
- **Фигурные скобки** обязательны для `if`/`for`/`while`
### JavaScript (Frontend)
#### Именование
| Категория | Стиль | Пример |
|-----------|-------|--------|
| Файлы | kebab-case | `main.js`, `schedule-view.js` |
| Функции и переменные | camelCase | `loadUsers()`, `pageTitle` |
| Константы | UPPER_SNAKE_CASE | `API_BASE_URL` |
#### Модули
- ES6 Modules с `import`/`export`
- **Всегда указывать расширение:** `import { api } from './api.js';`
#### Лучшие практики
```javascript
// ✅ Предпочитайте const
const token = localStorage.getItem('token');
// ✅ Async/await вместо .then()
async function loadData() {
try {
const data = await api.get('/api/users');
} catch (e) {
console.error('Ошибка:', e.message);
}
}
// ✅ Template literals
const msg = `Найдено ${items.length} записей`;
// ✅ Деструктуризация
const { id, name, role } = user;
```
#### Форматирование
- **Отступы:** 4 пробела
- **Кавычки:** одинарные `'`
- **Точки с запятой:** обязательны
---
## Создание нового эндпоинта (пошагово)
### 1. Модель (если нужна новая таблица)
Создайте Flyway миграцию `V{N}__{description}.sql`:
```sql
-- backend/src/main/resources/db/migration/V3__add_absences.sql
CREATE TABLE IF NOT EXISTS absences (
id BIGSERIAL PRIMARY KEY,
teacher_id BIGINT NOT NULL REFERENCES users(id),
reason VARCHAR(255) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL
);
```
Создайте JPA-сущность:
```java
@Entity
@Table(name = "absences")
public class Absence {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// ...
}
```
### 2. Репозиторий
```java
public interface AbsenceRepository extends JpaRepository<Absence, Long> {
List<Absence> findByTeacherId(Long teacherId);
}
```
### 3. DTO (опционально)
```java
public record AbsenceResponse(Long id, String teacherName, String reason) {}
```
### 4. Контроллер
```java
@RestController
@RequestMapping("/api/absences")
public class AbsenceController {
private final AbsenceRepository absenceRepository;
public AbsenceController(AbsenceRepository absenceRepository) {
this.absenceRepository = absenceRepository;
}
@GetMapping
public List<Absence> getAll() {
return absenceRepository.findAll();
}
}
```
---
## Работа с миграциями Flyway
### Правила
1. **Никогда** не изменяйте уже закоммиченные файлы миграций
2. Имя файла: `V{номер}__{описание}.sql` (два подчёркивания!)
3. Нумерация строго инкрементальная: `V1`, `V2`, `V3`, ...
4. После добавления — перезапустите backend для применения
### Применение
```bash
# Локально — сброс и повтор всех миграций
docker compose down -v && docker compose up -d
# Продакшн — применить к существующим тенантам
kubectl rollout restart deployment backend -n magistr
```
---
## Структура пакетов (Backend)
```
com.magistr.app/
├── Application.java # Точка входа
├── config/
│ ├── AppConfig.java # Бины (BCryptPasswordEncoder)
│ ├── DataInitializer.java # Инициализация данных
│ └── tenant/ # Мультитенантность
│ ├── TenantConfig.java # POJO конфигурации тенанта
│ ├── TenantContext.java # ThreadLocal текущего тенанта
│ ├── TenantInterceptor.java # Определение тенанта из Host
│ ├── TenantRoutingDataSource.java # Маршрутизация к БД
│ ├── TenantDataSourceConfig.java # Spring-конфигурация
│ ├── TenantConfigWatcher.java # Периодическая синхронизация
│ └── ConfigMapUpdater.java # Обновление K8s ConfigMap
├── controller/ # REST-контроллеры
│ ├── AuthController.java
│ ├── LessonsController.java
│ ├── ClassroomController.java
│ ├── DatabaseController.java
│ ├── UserController.java
│ ├── GroupController.java
│ ├── SubjectController.java
│ ├── EquipmentController.java
│ ├── EducationFormController.java
│ └── TeacherSubjectController.java
├── dto/ # Data Transfer Objects
├── model/ # JPA-сущности
├── repository/ # Spring Data JPA
└── utils/ # Валидаторы
├── DayAndWeekValidator.java
└── TypeAndFormatLessonValidator.java
```

203
docs/FRONTEND.md Normal file
View File

@@ -0,0 +1,203 @@
# 🎨 Frontend
## Общая информация
| Параметр | Значение |
|----------|----------|
| **Фреймворк** | Нет (Vanilla JavaScript) |
| **Модульная система** | ES6 Modules (`import`/`export`) |
| **Стили** | CSS (модульный подход) |
| **Шрифт** | [Inter](https://fonts.google.com/specimen/Inter) (Google Fonts) |
| **Веб-сервер** | Apache httpd:alpine |
---
## Структура файлов
```
frontend/
├── index.html # 🔐 Страница авторизации (общая)
├── script.js # Логика авторизации
├── style.css # Стили страницы авторизации
├── theme-toggle.js # Переключение светлой/тёмной темы
├── Dockerfile # httpd:alpine
├── admin/ # 👨‍💼 Интерфейс администратора
│ ├── index.html # SPA-оболочка с sidebar
│ ├── css/
│ │ ├── main.css # CSS-переменные, цвета, типографика
│ │ ├── layout.css # Раскладка (sidebar, topbar, content)
│ │ ├── components.css # Кнопки, таблицы, карточки, формы
│ │ └── modals.css # Модальные окна
│ ├── js/
│ │ ├── main.js # Инициализация, маршрутизация, навигация
│ │ ├── api.js # HTTP-обёртка (fetch + Authorization)
│ │ ├── utils.js # Утилиты
│ │ ├── otel.js # OpenTelemetry (клиентская телеметрия)
│ │ └── views/ # Модули представлений
│ │ ├── users.js # Управление пользователями
│ │ ├── groups.js # Управление группами
│ │ ├── classrooms.js # Управление аудиториями
│ │ ├── subjects.js # Управление дисциплинами
│ │ ├── equipments.js # Управление оборудованием
│ │ ├── edu-forms.js # Формы обучения
│ │ ├── schedule.js # Расписание занятий
│ │ └── database.js # Управление тенантами
│ └── views/ # HTML-шаблоны представлений
│ ├── users.html
│ ├── groups.html
│ ├── classrooms.html
│ ├── subjects.html
│ ├── equipments.html
│ ├── edu-forms.html
│ ├── schedule.html
│ └── database.html
├── teacher/ # 👩‍🏫 Интерфейс преподавателя
│ └── index.html # Просмотр расписания
└── student/ # 🎓 Интерфейс студента
└── index.html # Просмотр расписания (read-only)
```
---
## Система маршрутизации (Admin SPA)
Админ-панель работает как **Single Page Application** без фреймворка.
Навигация реализована через `data-tab` атрибуты на элементах sidebar:
```html
<a href="#" class="nav-item" data-tab="users">Пользователи</a>
<a href="#" class="nav-item" data-tab="groups">Группы</a>
<a href="#" class="nav-item" data-tab="schedule">Расписание занятий</a>
```
При клике на пункт меню `main.js`:
1. Загружает HTML-шаблон из `views/{tab}.html` через `fetch()`
2. Вставляет его в `#app-content`
3. Подключает соответствующий JS-модуль из `js/views/{tab}.js`
4. Обновляет заголовок страницы (`#page-title`)
### Разделы админ-панели
| Tab | Описание | API |
|-----|----------|-----|
| `users` | CRUD пользователей | `/api/users` |
| `groups` | CRUD групп | `/api/groups` |
| `edu-forms` | Формы обучения | `/api/education-forms` |
| `equipments` | Оборудование | `/api/equipments` |
| `classrooms` | Аудитории | `/api/classrooms` |
| `subjects` | Дисциплины | `/api/subjects` |
| `schedule` | Расписание | `/api/users/lessons` |
| `database` | Тенанты | `/api/database` |
---
## API-клиент (`api.js`)
Все HTTP-запросы проходят через обёртку `apiFetch()`:
```javascript
export async function apiFetch(endpoint, method = 'GET', body = null) {
const response = await fetch(endpoint, {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: body ? JSON.stringify(body) : null
});
if (!response.ok) {
throw new Error(data?.message || `Ошибка HTTP: ${response.status}`);
}
return await response.json();
}
// Shortcut-методы
export const api = {
get: (url) => apiFetch(url, 'GET'),
post: (url, body) => apiFetch(url, 'POST', body),
put: (url, body) => apiFetch(url, 'PUT', body),
delete: (url, body) => apiFetch(url, 'DELETE', body)
};
```
Токен берётся из `localStorage.getItem('token')`.
---
## Аутентификация (Frontend)
### Страница входа (`/index.html`)
1. Пользователь вводит логин/пароль
2. `script.js` отправляет `POST /api/auth/login`
3. При успехе сохраняет в `localStorage`:
- `token` — UUID-токен
- `role` — роль пользователя
4. Перенаправляет на соответствующий интерфейс:
- `ADMIN``/admin/`
- `TEACHER``/teacher/`
- `STUDENT``/student/`
### Проверка авторизации
На каждой странице проверяется наличие токена и роли:
```javascript
export function isAuthenticatedAsAdmin() {
const role = localStorage.getItem('role');
return token && role === 'ADMIN';
}
```
### Выход
Кнопка «Выйти» очищает `localStorage` и перенаправляет на `/`.
---
## CSS-архитектура
### Модульный подход
Стили разделены на 4 файла (порядок подключения важен):
1. **`main.css`** — CSS-переменные (цвета, шрифты, отступы), глобальные стили, тёмная тема
2. **`layout.css`** — Sidebar, topbar, content area, responsive
3. **`components.css`** — Кнопки, таблицы, карточки, badge, формы
4. **`modals.css`** — Модальные окна
### Темизация
CSS-переменные позволяют поддерживать светлую/тёмную тему:
```css
:root {
--bg-primary: #ffffff;
--text-primary: #1a1a2e;
--accent: #6366f1;
}
[data-theme="dark"] {
--bg-primary: #0f0f23;
--text-primary: #e2e8f0;
--accent: #818cf8;
}
```
Переключение — через `theme-toggle.js`.
---
## Адаптивность
Интерфейс адаптирован под мобильные устройства:
- Sidebar скрывается на экранах < 768px
- Появляется кнопка-гамбургер (`#menu-toggle`)
- Sidebar выезжает как overlay
- Таблицы получают горизонтальный скролл

137
docs/INFRASTRUCTURE.md Normal file
View File

@@ -0,0 +1,137 @@
# 🏭 Инфраструктура
## Docker Compose (локальная разработка)
### Сервисы
```yaml
services:
backend: # Spring Boot (Java 17), порт 8080
frontend: # Apache httpd:alpine, порт 80
db: # PostgreSQL alpine3.23, порт 5432
```
### Сеть
Все сервисы работают в Docker-сети `proxy` (external). Перед первым запуском:
```bash
docker network create proxy
```
### Переменные окружения
Файл `.env` в корне проекта:
```env
POSTGRES_USER=myuser
POSTGRES_PASSWORD=supersecretpassword
```
### Dockerfile (Backend)
Backend собирается через multi-stage сборку Maven:
1. Этап сборки: `maven:3-eclipse-temurin-17-alpine``mvn package`
2. Этап запуска: `eclipse-temurin:17-jre-alpine``java -jar app.jar`
### Dockerfile (Frontend)
```dockerfile
FROM httpd:alpine
COPY . /usr/local/apache2/htdocs/
RUN chown -R www-data:www-data /usr/local/apache2/htdocs/
```
---
## Kubernetes (продакшн)
### Расположение конфигурации
Файлы Kubernetes манифестов: `../k8s/`
### Ключевые ресурсы
| Ресурс | Тип | Описание |
|--------|-----|----------|
| `backend` | Deployment | Spring Boot приложение |
| `frontend` | Deployment | Apache httpd |
| `tenants-config` | ConfigMap | JSON-список тенантов |
### ConfigMap для тенантов
ConfigMap `tenants-config` монтируется в под backend по пути `/config/tenants.json`.
При добавлении тенанта через API:
1. `DatabaseController` обновляет in-memory DataSource
2. `ConfigMapUpdater` обновляет ConfigMap через Kubernetes API
3. `TenantConfigWatcher` на остальных подах подхватывает изменения (каждые 30 сек)
### Обновление backend
```bash
kubectl rollout restart deployment backend -n magistr
```
---
## Caddy (реверс-прокси)
**Расположение:** `../caddy-proxy/` для локальной разработки, в продакшене - отдельный сервис
В продакшене Caddy обрабатывает входящий трафик для `*.zuev.company`:
- Автоматическое получение TLS-сертификатов (Let's Encrypt)
- Маршрутизация `/api/*` → backend:8080
- Маршрутизация статики → frontend:80
---
## CI/CD (Gitea Actions)
### Пайплайн сборки Docker-образов
Расположение: `.gitea/workflows/docker-build.yaml`
Основные шаги:
1. Checkout кода
2. Login в Docker Registry
3. Build + Push образов (`backend`, `frontend`)
4. Генерация меток через `docker/metadata-action`
---
## Мониторинг (SigNoz + OpenTelemetry)
### Архитектура мониторинга
```mermaid
graph LR
Backend["Spring Boot"] -->|OTLP gRPC| Collector["OTel Collector"]
Frontend["JS (otel.js)"] -->|OTLP HTTP| Collector
Collector --> SigNoz["SigNoz"]
Collector -->|"Метрики PostgreSQL"| PgExporter["pg_exporter"]
```
### Интеграция Backend
Backend отправляет через OpenTelemetry:
- **Логи** — через Logback + OTLP exporter
- **Трейсы** — автоинструментация Spring Boot
- **Метрики** — JVM метрики, HTTP метрики
Tenant ID добавляется в:
- MDC (логи): `MDC.put("tenant.id", tenant)`
- Span атрибуты: `Span.current().setAttribute("tenant.id", tenant)`
### Интеграция Frontend
Файл `admin/js/otel.js` — клиентская телеметрия:
- Метрики производительности страниц
- Трейсы пользовательских действий
### Дашборды SigNoz
- JVM Dashboard (Heap, GC, Threads)
- PostgreSQL Dashboard (Connections, Queries)
- HTTP Dashboard (Requests, Latency, Errors)

167
docs/LOGGING.md Normal file
View File

@@ -0,0 +1,167 @@
# 📋 Логирование
## Стек технологий
| Компонент | Технология |
|-----------|------------|
| Фасад | SLF4J (`org.slf4j.Logger`) |
| Реализация | Logback (поставляется с `spring-boot-starter-web`) |
| Конфигурация | Стандартная Spring Boot (без кастомного `logback.xml`) |
| Экспорт (прод) | OpenTelemetry Java Agent → OTLP → SigNoz |
| Контекст тенанта | SLF4J MDC (`tenant.id`) |
---
## Архитектура
```mermaid
graph LR
Code["Java-код<br/>log.info(...)"] --> SLF4J["SLF4J API"]
SLF4J --> Logback["Logback"]
Logback -->|"Локальная разработка"| Console["stdout / stderr"]
Logback -->|"Продакшн"| OTelAgent["OTel Java Agent<br/>(Logback Appender)"]
OTelAgent -->|"OTLP HTTP"| SigNoz["SigNoz"]
```
### Локальная разработка
Логи выводятся в `stdout` контейнера в стандартном формате Spring Boot:
```
2026-03-22 12:00:00.123 INFO 1 --- [main] c.m.app.config.DataInitializer : Initializing databases for 1 tenant(s)...
```
Просмотр логов:
```bash
docker compose logs -f backend
```
### Продакшн (Kubernetes)
OpenTelemetry Java Agent подключается как `-javaagent` в [Dockerfile](file:///mnt/HDD/magistr/magistr/backend/Dockerfile) и автоматически перехватывает логи Logback, экспортируя их в SigNoz по OTLP.
```dockerfile
ENTRYPOINT ["java", "-javaagent:opentelemetry-javaagent.jar", "-jar", "app.jar"]
```
Конфигурация агента задаётся через переменные окружения в [backend.yaml](file:///mnt/HDD/magistr/k8s/backend.yaml):
| Переменная | Значение | Назначение |
|------------|----------|------------|
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://192.168.1.100:4318` | Адрес SigNoz Collector |
| `OTEL_SERVICE_NAME` | `magistr-backend` | Имя сервиса в SigNoz |
| `OTEL_RESOURCE_ATTRIBUTES` | `deployment.environment=default` | Окружение |
| `OTEL_LOGS_EXPORTER` | `otlp` | Экспорт логов через OTLP |
| `OTEL_METRICS_EXPORTER` | `otlp` | Экспорт метрик через OTLP |
| `OTEL_TRACES_EXPORTER` | `otlp` | Экспорт трейсов через OTLP |
| `OTEL_INSTRUMENTATION_LOGBACK_APPENDER_EXPERIMENTAL_CAPTURE_MDC_ATTRIBUTES` | `tenant.id` | Захват MDC-атрибута в логи |
> [!NOTE]
> В локальной разработке OpenTelemetry Agent также встроен в Docker-образ, но без переменных `OTEL_*` он работает в режиме noop — логи идут только в stdout.
---
## Мультитенантный контекст (MDC)
Каждый HTTP-запрос обогащается tenant ID через [TenantInterceptor](file:///mnt/HDD/magistr/magistr/backend/src/main/java/com/magistr/app/config/tenant/TenantInterceptor.java):
```java
// preHandle — при входе запроса
MDC.put("tenant.id", tenant);
Span.current().setAttribute("tenant.id", tenant);
// afterCompletion — после завершения
MDC.remove("tenant.id");
```
Это позволяет:
- Фильтровать логи по тенанту в SigNoz
- Коррелировать логи с трейсами через Span-атрибуты
- Идентифицировать, к какому университету относится каждая запись
---
## Использование в коде
### Классы с логированием
| Класс | Уровни | Что логируется |
|-------|--------|----------------|
| `TenantInterceptor` | DEBUG, WARN | Резолвинг тенанта, неизвестный тенант (404) |
| `TenantDataSourceConfig` | INFO, WARN, ERROR | Загрузка тенантов, fallback на H2 |
| `TenantRoutingDataSource` | INFO, WARN | Добавление/удаление тенантов, тест соединения |
| `TenantConfigWatcher` | INFO, ERROR, WARN | Изменения ConfigMap, Flyway миграции |
| `ConfigMapUpdater` | INFO, WARN, ERROR | Обновление ConfigMap в K8s |
| `DataInitializer` | INFO | Инициализация БД при старте |
| `LessonsController` | INFO, DEBUG, ERROR | CRUD-операции с занятиями, валидация |
### Паттерн использования
```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyClass {
private static final Logger log = LoggerFactory.getLogger(MyClass.class);
public void doSomething() {
log.info("Операция выполнена: param={}", value);
log.error("Ошибка: {}", e.getMessage(), e); // со стектрейсом
}
}
```
### Рекомендации по уровням
| Уровень | Когда использовать |
|---------|-------------------|
| `ERROR` | Необработанные ошибки, сбои подключения к БД, провалы миграций |
| `WARN` | Неизвестный тенант, нет конфигурации, fallback-сценарии |
| `INFO` | Успешные операции, CRUD-действия, старт/стоп компонентов |
| `DEBUG` | Детали резолвинга тенанта, ping-запросы |
---
## Настройка уровня логирования
В [application.properties](file:///mnt/HDD/magistr/magistr/backend/src/main/resources/application.properties) (по умолчанию закомментировано):
```properties
# Включить DEBUG для всего приложения
#logging.level.root=DEBUG
# Только для пакета приложения
logging.level.com.magistr.app=DEBUG
# Только для конкретного класса
logging.level.com.magistr.app.config.tenant.TenantInterceptor=DEBUG
```
Также можно задавать через переменные окружения:
```bash
LOGGING_LEVEL_ROOT=DEBUG
LOGGING_LEVEL_COM_MAGISTR_APP=DEBUG
```
---
## Просмотр логов
### Локально (Docker Compose)
```bash
# Все логи backend
docker compose logs -f backend
# Фильтрация по ключевому слову
docker compose logs -f backend | grep "tenant"
```
### Продакшн (SigNoz)
Логи доступны в веб-интерфейсе SigNoz → раздел **Logs**:
- Фильтрация по `service.name = magistr-backend`
- Фильтрация по `tenant.id` (из MDC)
- Корреляция с трейсами через общий `trace_id`

113
docs/README.md Normal file
View File

@@ -0,0 +1,113 @@
# 📚 Magistr — Система управления университетским расписанием
## Обзор
**Magistr** — веб-приложение для управления расписанием занятий университета. Система поддерживает мультитенантную архитектуру (каждый университет = отдельная база данных), ролевую модель доступа (Администратор, Преподаватель, Студент) и полное управление аудиторным фондом, группами, дисциплинами и преподавательским составом.
- **Продакшн:** [https://magistr.zuev.company](https://magistr.zuev.company)
- **Локальная разработка:** [http://localhost:80](http://localhost:80)
---
## Стек технологий
| Компонент | Технология |
|-----------|-----------|
| **Backend** | Java 17, Spring Boot 3.2.5 |
| **Frontend** | Vanilla JavaScript (ES6 Modules) + HTML/CSS |
| **База данных** | PostgreSQL (через Flyway миграции) |
| **Контейнеризация** | Docker, Docker Compose |
| **Продакшн** | Kubernetes, Caddy (реверс-прокси) |
| **Мониторинг** | SigNoz, OpenTelemetry |
| **CI/CD** | Gitea Actions |
---
## Быстрый старт
### Предварительные требования
- Docker и Docker Compose
- Git
### Локальный запуск
```bash
# 1. Клонировать репозиторий
git clone <repo-url> magistr && cd magistr
# 2. Создать Docker-сеть (если ещё не создана)
docker network create proxy
# 3. Запустить все сервисы
docker compose up -d --build
```
После запуска приложение доступно по адресу: **http://localhost:80**
**Учётные данные по умолчанию:**
| Логин | Пароль | Роль |
|-------|--------|------|
| `admin` | `admin` | Администратор |
| `Тестовый преподаватель` | `1234567890` | Преподаватель |
### Полезные команды
```bash
# Просмотр логов
docker compose logs -f backend
# Полный сброс базы данных (удаление данных + повтор миграций)
docker compose down -v
docker compose up -d
# Остановка всех сервисов
docker compose down
```
---
## Структура проекта
```
magistr/
├── backend/ # Java Spring Boot backend
│ └── src/main/
│ ├── java/com/magistr/app/
│ │ ├── controller/ # REST-контроллеры (10 шт.)
│ │ ├── model/ # JPA-сущности
│ │ ├── dto/ # Data Transfer Objects
│ │ ├── repository/ # Spring Data JPA репозитории
│ │ ├── config/ # Конфигурация приложения
│ │ │ └── tenant/ # Мультитенантность
│ │ └── utils/ # Валидаторы
│ └── resources/
│ ├── application.properties
│ └── db/migration/ # Flyway SQL миграции
├── frontend/ # Статический фронтенд
│ ├── index.html # Страница авторизации
│ ├── admin/ # Админ-панель (деканат)
│ │ ├── js/views/ # Модули представлений
│ │ └── css/ # Стили
│ ├── teacher/ # Интерфейс преподавателя
│ └── student/ # Интерфейс студента
├── docs/ # 📖 Документация (вы здесь)
├── compose.yaml # Docker Compose конфигурация
├── .env # Переменные окружения
└── AGENTS.md # Руководство для AI-агентов
```
---
## 📖 Навигация по документации
| Документ | Содержание |
|----------|-----------|
| [Архитектура](ARCHITECTURE.md) | Общая архитектура, мультитенантность, взаимодействие компонентов |
| [Бизнес-логика](BUSINESS_LOGIC.md) | Ролевая модель, правила расписания, управление ресурсами |
| [База данных](DATABASE.md) | Схема БД, описание таблиц, Flyway миграции |
| [REST API](API.md) | Все эндпоинты с примерами запросов и ответов |
| [Инфраструктура](INFRASTRUCTURE.md) | Docker, Kubernetes, CI/CD, мониторинг |
| [Разработка](DEVELOPMENT.md) | Code Style, соглашения, инструкции для разработчиков |
| [Frontend](FRONTEND.md) | Архитектура фронтенда, модули, стили |

View File

@@ -0,0 +1,11 @@
/* Стили для формы создания кафедр и специальностей */
.departments-data-icon {
margin-right: 0.5rem;
}
#departments-tbody .loading-row,
#specialties-tbody .loading-row {
text-align: center;
color: var(--text-muted);
padding: 2rem;
}

View File

@@ -15,6 +15,7 @@
<link rel="stylesheet" href="css/components.css"> <link rel="stylesheet" href="css/components.css">
<link rel="stylesheet" href="css/modals.css"> <link rel="stylesheet" href="css/modals.css">
<link rel="stylesheet" href="css/department.css"> <link rel="stylesheet" href="css/department.css">
<link rel="stylesheet" href="css/departments-data.css">
</head> </head>
<body> <body>
@@ -58,6 +59,13 @@
</svg> </svg>
Кафедра Кафедра
</a> </a>
<a href="#" class="nav-item" data-tab="departments-data">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect>
</svg>
Создание кафедры/специальности
</a>
<a href="#" class="nav-item" data-tab="groups"> <a href="#" class="nav-item" data-tab="groups">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"> stroke-linecap="round" stroke-linejoin="round">

View File

@@ -1,3 +1,5 @@
import './otel.js';
import { isAuthenticatedAsAdmin } from './api.js'; import { isAuthenticatedAsAdmin } from './api.js';
import { applyRippleEffect, closeAllDropdownsOnOutsideClick } from './utils.js'; import { applyRippleEffect, closeAllDropdownsOnOutsideClick } from './utils.js';
@@ -10,6 +12,7 @@ import { initSubjects } from './views/subjects.js';
import {initSchedule} from "./views/schedule.js"; import {initSchedule} from "./views/schedule.js";
import {initDatabase} from "./views/database.js"; import {initDatabase} from "./views/database.js";
import {initDepartment} from "./views/department.js"; import {initDepartment} from "./views/department.js";
import {initDepartmentsData} from "./views/departments-data.js";
// Configuration // Configuration
const ROUTES = { const ROUTES = {
@@ -22,6 +25,7 @@ const ROUTES = {
schedule: { title: 'Расписание занятий', file: 'views/schedule.html', init: initSchedule }, schedule: { title: 'Расписание занятий', file: 'views/schedule.html', init: initSchedule },
database: { title: 'База данных', file: 'views/database.html', init: initDatabase }, database: { title: 'База данных', file: 'views/database.html', init: initDatabase },
department: { title: 'Кафедры', file: 'views/department.html', init: initDepartment }, department: { title: 'Кафедры', file: 'views/department.html', init: initDepartment },
'departments-data': { title: 'Создание кафедры/специальности', file: 'views/departments-data.html', init: initDepartmentsData },
}; };
let currentTab = null; let currentTab = null;

47
frontend/admin/js/otel.js Normal file
View File

@@ -0,0 +1,47 @@
import { WebTracerProvider } from 'https://esm.sh/@opentelemetry/sdk-trace-web@1.22.0';
import { getWebAutoInstrumentations } from 'https://esm.sh/@opentelemetry/auto-instrumentations-web@0.37.0';
import { OTLPTraceExporter } from 'https://esm.sh/@opentelemetry/exporter-trace-otlp-http@0.49.1';
import { BatchSpanProcessor } from 'https://esm.sh/@opentelemetry/sdk-trace-base@1.22.0';
import { registerInstrumentations } from 'https://esm.sh/@opentelemetry/instrumentation@0.49.1';
import { ZoneContextManager } from 'https://esm.sh/@opentelemetry/context-zone@1.22.0';
import { Resource } from 'https://esm.sh/@opentelemetry/resources@1.22.0';
import { SemanticResourceAttributes } from 'https://esm.sh/@opentelemetry/semantic-conventions@1.22.0';
// Инициализация провайдера метрик и трейсов с именем сервиса
const provider = new WebTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'magistr-frontend-admin',
}),
});
// Экспортер отправляет данные на относительный путь /otel/v1/traces.
// На проде Caddy перехватит этот запрос и проксирует в SigNoz OTLP Collector (порт 4318).
const traceExporter = new OTLPTraceExporter({
url: window.location.origin + '/otel/v1/traces',
});
// Использование BatchSpanProcessor для буферизации трейсов перед отправкой
provider.addSpanProcessor(new BatchSpanProcessor(traceExporter));
// Использование ZoneContextManager для поддержки асинхронных операций (Promise, setTimeout, etc)
provider.register({
contextManager: new ZoneContextManager(),
});
// Регистрация авто-инструментаций для бразуера (document-load, xml-http-request, fetch, history, etc)
registerInstrumentations({
instrumentations: [
getWebAutoInstrumentations({
'@opentelemetry/instrumentation-fetch': {
propagateTraceHeaderCorsUrls: /.*/,
clearTimingResources: true,
},
'@opentelemetry/instrumentation-xml-http-request': {
propagateTraceHeaderCorsUrls: /.*/,
clearTimingResources: true,
},
}),
],
});
console.log('OpenTelemetry Web SDK initialized successfully.');

View File

@@ -0,0 +1,103 @@
import { api } from '../api.js';
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
export async function initDepartmentsData() {
const deptTbody = document.getElementById('departments-tbody');
const specTbody = document.getElementById('specialties-tbody');
const createDeptForm = document.getElementById('create-department-form');
const createSpecForm = document.getElementById('create-specialty-form');
let departments = [];
let specialties = [];
async function loadData() {
// Load Departments
try {
departments = await api.get('/api/departments');
renderDepartments();
} catch (e) {
deptTbody.innerHTML = '<tr><td colspan="3" class="loading-row">-</td></tr>';
}
// Load Specialties
try {
specialties = await api.get('/api/specialties');
renderSpecialties();
} catch (e) {
specTbody.innerHTML = '<tr><td colspan="3" class="loading-row">-</td></tr>';
}
}
function renderDepartments() {
if (!departments || !departments.length) {
deptTbody.innerHTML = '<tr><td colspan="3" class="loading-row">-</td></tr>';
return;
}
deptTbody.innerHTML = departments.map(d => `
<tr>
<td>${d.id}</td>
<td>${escapeHtml(d.departmentName || d.name)}</td>
<td>${escapeHtml(String(d.departmentCode || d.code))}</td>
</tr>
`).join('');
}
function renderSpecialties() {
if (!specialties || !specialties.length) {
specTbody.innerHTML = '<tr><td colspan="3" class="loading-row">-</td></tr>';
return;
}
specTbody.innerHTML = specialties.map(s => `
<tr>
<td>${s.id}</td>
<td>${escapeHtml(s.specialityName || s.name)}</td>
<td>${escapeHtml(s.specialityCode || s.specialtyCode || s.specialty_code)}</td>
</tr>
`).join('');
}
createDeptForm.addEventListener('submit', async (e) => {
e.preventDefault();
hideAlert('create-dept-alert');
const name = document.getElementById('dept-name').value.trim();
const code = document.getElementById('dept-code').value.trim();
if (!name || !code) {
showAlert('create-dept-alert', 'Заполните все поля', 'error');
return;
}
try {
await api.post('/api/departments', { departmentName: name, departmentCode: Number(code) });
showAlert('create-dept-alert', `Кафедра "${name}" создана`, 'success');
createDeptForm.reset();
loadData();
} catch (error) {
showAlert('create-dept-alert', error.message || 'Ошибка создания кафедры', 'error');
}
});
createSpecForm.addEventListener('submit', async (e) => {
e.preventDefault();
hideAlert('create-spec-alert');
const name = document.getElementById('spec-name').value.trim();
const code = document.getElementById('spec-code').value.trim();
if (!name || !code) {
showAlert('create-spec-alert', 'Заполните все поля', 'error');
return;
}
try {
await api.post('/api/specialties', { specialityName: name, specialityCode: code });
showAlert('create-spec-alert', `Специальность "${name}" создана`, 'success');
createSpecForm.reset();
loadData();
} catch (error) {
showAlert('create-spec-alert', error.message || 'Ошибка создания специальности', 'error');
}
});
loadData();
}

View File

@@ -0,0 +1,80 @@
<!-- ===== Departments and Specialties Tab ===== -->
<div class="card create-card">
<h2>Создание кафедры</h2>
<form id="create-department-form">
<div class="form-row">
<div class="form-group">
<label for="dept-name">Название кафедры</label>
<input type="text" id="dept-name" placeholder="Например: Кафедра ИБ" required>
</div>
<div class="form-group">
<label for="dept-code">Код кафедры</label>
<input type="number" id="dept-code" placeholder="Например: 1" required>
</div>
<button type="submit" class="btn-primary">Создать</button>
</div>
<div class="form-alert" id="create-dept-alert" role="alert"></div>
</form>
</div>
<div class="card">
<div class="card-header-row">
<h2>Кафедры</h2>
</div>
<div class="table-wrap">
<table id="departments-table">
<thead>
<tr>
<th>ID</th>
<th>Название</th>
<th>Код</th>
</tr>
</thead>
<tbody id="departments-tbody">
<tr>
<td colspan="3" class="loading-row">Загрузка...</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card create-card">
<h2>Создание специальности</h2>
<form id="create-specialty-form">
<div class="form-row">
<div class="form-group">
<label for="spec-name">Название специальности</label>
<input type="text" id="spec-name" placeholder="Например: Программная инженерия" required>
</div>
<div class="form-group">
<label for="spec-code">Код специальности</label>
<input type="text" id="spec-code" placeholder="Например: 09.03.04" required>
</div>
<button type="submit" class="btn-primary">Создать</button>
</div>
<div class="form-alert" id="create-spec-alert" role="alert"></div>
</form>
</div>
<div class="card">
<div class="card-header-row">
<h2>Специальности</h2>
</div>
<div class="table-wrap">
<table id="specialties-table">
<thead>
<tr>
<th>ID</th>
<th>Название</th>
<th>Код специальности</th>
</tr>
</thead>
<tbody id="specialties-tbody">
<tr>
<td colspan="3" class="loading-row">Загрузка...</td>
</tr>
</tbody>
</table>
</div>
</div>