From 40be00656026f55406dfb6135e677ff89aadece5 Mon Sep 17 00:00:00 2001 From: Arswarog Date: Tue, 16 Jun 2026 13:54:46 +0300 Subject: [PATCH] DEMO docs --- docs/01-intro.md | 51 +++++ docs/02-platform-overview.md | 42 ++++ docs/03-roles.md | 28 +++ docs/04-routes.md | 60 ++++++ docs/05-workflow/_category_.yml | 1 + docs/10-landings/_category_.yml | 1 + .../01-contests/01-create-contest.md | 38 ++++ .../01-contests/02-categories/01-intro.md | 59 +++++ .../02-categories/02-functional.md | 13 ++ .../01-contests/02-categories/_category_.yml | 1 + docs/30-modules/01-contests/_category_.yml | 1 + docs/30-modules/01-intro.md | 13 ++ .../30-modules/02-organizators/_category_.yml | 1 + .../02-organizators/organizators.md | 26 +++ docs/30-modules/05-applications/01-intro.md | 45 ++++ docs/30-modules/05-applications/02-states.md | 72 +++++++ .../05-applications/03-revisions.md | 27 +++ .../30-modules/05-applications/_category_.yml | 1 + docs/30-modules/06-requests/_category_.yml | 1 + docs/30-modules/06-requests/participants.md | 99 +++++++++ docs/30-modules/06-requests/projects.md | 98 +++++++++ docs/30-modules/06-requests/requests.md | 24 +++ docs/30-modules/09-evaluation/01-overview.md | 17 ++ .../09-evaluation/02-evaluation-form.md | 20 ++ .../09-evaluation/03-expert-assignment.md | 36 ++++ .../09-evaluation/04-project-evaluation.md | 12 ++ .../09-evaluation/05-review-analysis.md | 60 ++++++ docs/30-modules/09-evaluation/_category_.yml | 1 + docs/30-modules/10-statements/_category_.yml | 1 + docs/30-modules/10-statements/intro.md | 11 + docs/30-modules/_category_.yml | 1 + docs/40-guidelines/_category_.yml | 1 + docs/40-guidelines/modal.md | 202 ++++++++++++++++++ docs/50-developing/01-quick-start.md | 61 ++++++ docs/50-developing/02-branches.md | 58 +++++ docs/50-developing/03-files.md | 88 ++++++++ docs/50-developing/10-modules.md | 58 +++++ docs/50-developing/20-database.md | 110 ++++++++++ docs/50-developing/_category_.yml | 1 + docs/55-logging/01-overview.md | 67 ++++++ docs/55-logging/02-using.md | 91 ++++++++ docs/55-logging/03-auto-logging.md | 106 +++++++++ docs/55-logging/04-manual-logs.md | 123 +++++++++++ docs/55-logging/05-testing.md | 160 ++++++++++++++ docs/55-logging/_category_.yml | 1 + docs/60-common-modules/_category_.yml | 1 + docs/60-common-modules/auth/_category_.yml | 1 + docs/60-common-modules/auth/intro.md | 157 ++++++++++++++ docs/60-common-modules/configs.md | 31 +++ .../data-store/00-overview.md | 61 ++++++ docs/60-common-modules/data-store/01-usage.md | 98 +++++++++ .../data-store/02-testing.md | 55 +++++ .../data-store/_category_.yml | 1 + docs/60-common-modules/docs.md | 56 +++++ docs/70-common-ui/_category_.yml | 1 + docs/70-common-ui/tabs.md | 84 ++++++++ docs/90-deploy/01-ci-cd.md | 64 ++++++ docs/90-deploy/_category_.yml | 1 + 58 files changed, 2599 insertions(+) create mode 100644 docs/01-intro.md create mode 100644 docs/02-platform-overview.md create mode 100644 docs/03-roles.md create mode 100644 docs/04-routes.md create mode 100644 docs/05-workflow/_category_.yml create mode 100644 docs/10-landings/_category_.yml create mode 100644 docs/30-modules/01-contests/01-create-contest.md create mode 100644 docs/30-modules/01-contests/02-categories/01-intro.md create mode 100644 docs/30-modules/01-contests/02-categories/02-functional.md create mode 100644 docs/30-modules/01-contests/02-categories/_category_.yml create mode 100644 docs/30-modules/01-contests/_category_.yml create mode 100644 docs/30-modules/01-intro.md create mode 100644 docs/30-modules/02-organizators/_category_.yml create mode 100644 docs/30-modules/02-organizators/organizators.md create mode 100644 docs/30-modules/05-applications/01-intro.md create mode 100644 docs/30-modules/05-applications/02-states.md create mode 100644 docs/30-modules/05-applications/03-revisions.md create mode 100644 docs/30-modules/05-applications/_category_.yml create mode 100644 docs/30-modules/06-requests/_category_.yml create mode 100644 docs/30-modules/06-requests/participants.md create mode 100644 docs/30-modules/06-requests/projects.md create mode 100644 docs/30-modules/06-requests/requests.md create mode 100644 docs/30-modules/09-evaluation/01-overview.md create mode 100644 docs/30-modules/09-evaluation/02-evaluation-form.md create mode 100644 docs/30-modules/09-evaluation/03-expert-assignment.md create mode 100644 docs/30-modules/09-evaluation/04-project-evaluation.md create mode 100644 docs/30-modules/09-evaluation/05-review-analysis.md create mode 100644 docs/30-modules/09-evaluation/_category_.yml create mode 100644 docs/30-modules/10-statements/_category_.yml create mode 100644 docs/30-modules/10-statements/intro.md create mode 100644 docs/30-modules/_category_.yml create mode 100644 docs/40-guidelines/_category_.yml create mode 100644 docs/40-guidelines/modal.md create mode 100644 docs/50-developing/01-quick-start.md create mode 100644 docs/50-developing/02-branches.md create mode 100644 docs/50-developing/03-files.md create mode 100644 docs/50-developing/10-modules.md create mode 100644 docs/50-developing/20-database.md create mode 100644 docs/50-developing/_category_.yml create mode 100644 docs/55-logging/01-overview.md create mode 100644 docs/55-logging/02-using.md create mode 100644 docs/55-logging/03-auto-logging.md create mode 100644 docs/55-logging/04-manual-logs.md create mode 100644 docs/55-logging/05-testing.md create mode 100644 docs/55-logging/_category_.yml create mode 100644 docs/60-common-modules/_category_.yml create mode 100644 docs/60-common-modules/auth/_category_.yml create mode 100644 docs/60-common-modules/auth/intro.md create mode 100644 docs/60-common-modules/configs.md create mode 100644 docs/60-common-modules/data-store/00-overview.md create mode 100644 docs/60-common-modules/data-store/01-usage.md create mode 100644 docs/60-common-modules/data-store/02-testing.md create mode 100644 docs/60-common-modules/data-store/_category_.yml create mode 100644 docs/60-common-modules/docs.md create mode 100644 docs/70-common-ui/_category_.yml create mode 100644 docs/70-common-ui/tabs.md create mode 100644 docs/90-deploy/01-ci-cd.md create mode 100644 docs/90-deploy/_category_.yml diff --git a/docs/01-intro.md b/docs/01-intro.md new file mode 100644 index 0000000..88f1b43 --- /dev/null +++ b/docs/01-intro.md @@ -0,0 +1,51 @@ +# Общее описание + +Платформа для грантов предназначена для упрощения и автоматизации процесса распределения грантовых +средств. Она предоставляет удобный инструмент как для организаций, желающих вложиться в общественно +полезные проекты, так и для участников, нуждающихся в финансировании. Основные задачи платформы: + +- **Прозрачность:** Платформа обеспечивает открытый процесс подачи заявок, их оценки и выбора + победителей. + +- **Доступность:** Участники могут легко находить актуальные конкурсы и подавать заявки, а + компании — запускать собственные грантовые программы. + +- **Эффективность:** Процесс отбора и реализации проектов автоматизирован, что экономит время + участников и организаторов. + +## Основные пользователи платформы: + +- **Компании:** Организуют конкурсы, финансируют проекты. + +- **Участники:** Представляют свои проекты, чтобы получить финансирование. При подаче заявки + участники выбирают категорию, что влияет на структуру заявки и условия участия. + +- **Модераторы:** Проверяют заявки, следят за корректностью информации. + +- **Эксперты:** Оценивают проекты, прошедшие модерацию, с учетом их категории. + +- **Комиссия:** Принимает окончательные решения о финансировании. + +- **Администраторы:** Управляют платформой и обеспечивают её стабильную работу, включая создание и + настройку категорий. + +## Основные этапы работы платформы: + +1. Регистрация пользователей (компании, организации, волонтеры). + +2. Публикация конкурсов + +3. Сбор заявок. На этапе подачи заявки пользователь выбирает категорию, что определяет дальнейшую + структуру заявки и условия участия. + +4. Модерация заявок и внесение исправлений участниками. + +5. Оценка проектов экспертами с учетом специфики категории. + +6. Выбор победителей комиссией. + +7. Реализация проектов победителями, включая публикацию отчетов. + +Платформа также включает инструменты для контроля исполнения проектов, анализа их результатов и +формирования отчетности. Это позволяет компаниям видеть, как эффективно используются их средства, а +участникам — демонстрировать успешность своих инициатив. diff --git a/docs/02-platform-overview.md b/docs/02-platform-overview.md new file mode 100644 index 0000000..f7e57a7 --- /dev/null +++ b/docs/02-platform-overview.md @@ -0,0 +1,42 @@ +# Основные функции платформы + +## Для участников + +- **Поиск конкурсов:** Участники могут просматривать доступные конкурсы. + +- **Подача заявок:** Удобный интерфейс для создания и отправки заявок на участие в конкурсе. + +- **Управление проектами:** Ведение проектов, отслеживание их статуса и предоставление отчетности. + +- **Финансовая отчетность:** Предоставление данных о расходах по проекту через встроенные формы. + +## Для организаторов + +- **Создание конкурсов:** Возможность настроить параметры конкурса (цели, требования, сроки и + др.). + +- **Управление заявками:** Просмотр всех поступивших заявок, их модерация и одобрение. + +- **Работа с экспертами:** Назначение экспертов для оценки заявок, управление их доступами. + +- **Мониторинг реализации проектов:** Контроль выполнения грантовых обязательств победителей. + +## Для экспертов + +- **Оценка заявок:** Просмотр и оценивание заявок участников по заданным критериям. Возможность + оставлять комментарии и замечания. + +## Для модераторов + +- **Проверка заявок:** Проверка заявок участников на соответствие требованиям конкурса. + +- **Коммуникация с участниками:** Возможность запрашивать доработки заявок и уведомлять участников + об изменениях статуса. + +## Для администраторов платформы + +- **Управление пользователями:** Добавление, редактирование и удаление пользователей. + +- **Мониторинг активности:** Анализ активности на платформе, выявление проблемных мест. + +- **Настройка глобальных параметров:** Конфигурация технических аспектов работы системы. diff --git a/docs/03-roles.md b/docs/03-roles.md new file mode 100644 index 0000000..d1585a9 --- /dev/null +++ b/docs/03-roles.md @@ -0,0 +1,28 @@ +# Роли пользователей + +## Глобальные роли + +Эти роли может выдать только `root` пользователь: + +- `root` Максимальный уровень доступа. С этим уровнем доступа можно обходить некоторые системы + сайта. Предполагается, что этот пользователь знает, что делает. +- `support` Имеет доступ к большому количеству данных и функций, может влиять на них, но с + ограничениями в критически важных областях. + +## Сотрудники конкурса + +Эти роли могут быть выданы как на отдельные конкурсы, так и глобально на все конкурсы: + +- `admin` Администратор конкурса. Может настраивать конкурс и сотрудников, имеет права `moderator` +- `moderator` Просмотр и модерирование проектов, просмотр статистики конкурса, возможность + блокировки пользователей. +- `expert` Оценка проектов без доступа к настройкам или модерации. + +## Сотрудники организатора + +Эти роли выдаются только на конкретные организации: + +- `orgAdminRole` Руководитель организации. Может настраивать организацию и её сотрудников. +- `orgMemberRole` Ответственный за заполнение проектов и отчетов. +- `orgReportedRole` Публикация новостей от имени организации. +- `orgReaderRole` Подписчик, имеет только права на просмотр информации. diff --git a/docs/04-routes.md b/docs/04-routes.md new file mode 100644 index 0000000..4d99389 --- /dev/null +++ b/docs/04-routes.md @@ -0,0 +1,60 @@ +# Разделы сайта + +## Кабинет участника + +Доступен всем авторизованным пользователям. + +Предназначен для сотрудников организаций, участвующих в конкурсах. + +### Точка входа `/cabinet` + +- `/cabinet/projects` Текущие проекты +- `/cabinet/projects/create` Создание нового проекта +- `/cabinet/orgs` Список организаций, в которых пользователь является сотрудником + +--- + +## Управление конкурсом + +Имеют доступ только сотрудники конкурса, `admin` и `support`. + +### Точка входа `/contests` + +- `/contests` выбор конкурса, отображает те конкурсы, в которых пользователь является сотрудником +- `/contests/[contestId]` Панель управления конкурсом. Во вложенных страницах можно управлять + проектами, заявками, периодами и областями конкурса +- `/contests/[contestId]/moderation` Модерация заявок + +--- + +## Администрирование сайта + +Имеют доступ только `admin` и `support`. + +### Точка входа `/admin` + +- `/admin` Панель администратора + +### Управление пользователями + +- `/admin/users` Список и поиск пользователей + +--- + +## Общие страницы + +На такие страницы может попасть любой пользователь, если есть разрешение. + +### Авторизация и регистрация + +- `/auth/login` Страница авторизации +- `/auth/reg` Страница регистрации +- `/profile` Страница профиля текущего пользователя + +### Пользователи + +- `/users/[userId]` Страница отдельного пользователя + +### Проекты + +- `/projects/[projectId]` Просмотр и редактирование проекта diff --git a/docs/05-workflow/_category_.yml b/docs/05-workflow/_category_.yml new file mode 100644 index 0000000..4db6ee6 --- /dev/null +++ b/docs/05-workflow/_category_.yml @@ -0,0 +1 @@ +label: 'Процесс работы' diff --git a/docs/10-landings/_category_.yml b/docs/10-landings/_category_.yml new file mode 100644 index 0000000..633e3df --- /dev/null +++ b/docs/10-landings/_category_.yml @@ -0,0 +1 @@ +label: 'Настройка и подключение лендингов' diff --git a/docs/30-modules/01-contests/01-create-contest.md b/docs/30-modules/01-contests/01-create-contest.md new file mode 100644 index 0000000..846ed0b --- /dev/null +++ b/docs/30-modules/01-contests/01-create-contest.md @@ -0,0 +1,38 @@ +# Создание конкурса + +Доступно создание конкурса с нуля или путём копирования ранее проведённого конкурса. + +## Создание черновика конкурса + +Создание черновика конкурса требует указания следующих сведений: + +- **Название конкурса** +- **Администратор конкурса** + +После создания черновика назначенный администратор получит доступ к панели управления конкурса и к +мастеру настройки. + +## Мастер настройки конкурса + +Мастер настройки обеспечивает последовательность действий и визуальный контроль прогресса: + +- **Статус модулей**. Индикация того, какие модули уже настроены, а какие остаются в работе. +- **Краткая информация**. Краткое описание каждого модуля и текущий статус настройки. +- **Переход к настройке**. Быстрый переход к настройке конкретного модуля. + +> **Важно:** Запуск конкурса становится доступным только после того, как все модули будут отмечены +> как "настроенные". После старта мастер настройки завершает работу и закрывается. + +## Варианты создания конкурса + +### Настройка с нуля + +Для некоторых модулей мастер может предоставлять инструменты для быстрого первого заполнения, +позволяя задать основные параметры и оставить детали на более поздний этап. Если такие инструменты +недоступны, настройка осуществляется стандартными средствами модуля. + +### Копирование существующего конкурса + +Для некоторых модулей мастер может предложить выбор элементов для переноса из ранее проведённого +конкурса. Это ускоряет первоначальную конфигурацию. Если инструмент копирования недоступен, +применяются обычные средства модуля для настройки существующего конкурса. diff --git a/docs/30-modules/01-contests/02-categories/01-intro.md b/docs/30-modules/01-contests/02-categories/01-intro.md new file mode 100644 index 0000000..a9a1954 --- /dev/null +++ b/docs/30-modules/01-contests/02-categories/01-intro.md @@ -0,0 +1,59 @@ +# Категории проектов в конкурсах + +Категории позволяют учитывать специфику проектов и их участников. Пользователи сами выбирают +категорию, в которой хотят участвовать и заполняют соответствующую форму заявки. + +#### Примеры категорий и их отличий: + +- **Волонтерские проекты:** Ограничения по срокам проведения, максимальная сумма грантов, + упрощенные формы заявки. + +- **Школьные проекты:** Участниками могут быть только группы школьников, дополнительные требования + для школ. + +- **Проекты организаций:** Более сложные формы заявки, большие максимальные суммы грантов. + +### Функционал категорий + +- **Создание и управление категориями:** + + - Только администраторы конкурса могут управлять категориями. + - Администраторы могут добавлять, изменять и удалять категории. + +- **Параметры категории:** + + - Условия участия (например, ограничения по типу участников, возрасту, региону). + - Формы заявки (разделы, поля, инструкции). + - Параметры конкурсов (сроки подачи, суммы грантов). + - И так далее. + +- **Гибкость изменения:** Возможность адаптации категорий для конкретных конкурсов. + +### Технические особенности + +- У категорий нет версионирования. +- Изменять категории можно в любой момент до завершения конкурса. +- Категория относится только к одному конкурсу. +- Участники должны видеть только public категории. +- Участник не может сменить категории после ее выбора. +- Проект может относиться только к одной категории. + +### Отчетность по категориям + +В разделе аналитики предоставляется возможность фильтровать заявки и результаты по категориям, +анализировать успешность проектов в каждой категории и их соответствие целям конкурсов. + +### Доступность категорий + +Для управления доступностью категории для создания проектов с этой категорией используется поле +`access`. + +Любую категорию можно включить или выключить для создания нового проекта с этой категорией. + +- `enabled: true` Участники могут создавать проекты в этой категории. +- `enabled: false` Участники не могут создавать проекты в этой категории. + +Флаг `manualControl` управляет способом изменения поля `enabled`: + +- `true` - включается и выключается только вручную. +- `false` - включается и выключается автоматически, на основании других настроек категории. diff --git a/docs/30-modules/01-contests/02-categories/02-functional.md b/docs/30-modules/01-contests/02-categories/02-functional.md new file mode 100644 index 0000000..61a242c --- /dev/null +++ b/docs/30-modules/01-contests/02-categories/02-functional.md @@ -0,0 +1,13 @@ +# Функционал категорий + +Категории предоставляют гибкость настройки условий для разных типов проектов. Каждая категория может +изменять следующие параметры: + +- Максимальная сумма гранта +- Максимальный фонд гранта +- Условия участия (текст и документ) +- Требования к участникам (текст и документ) +- Сроки подачи заявок для данной категории +- Направления проектов +- Форма заявки +- Список документов, которые могут потребоваться участникам от организатора конкурса diff --git a/docs/30-modules/01-contests/02-categories/_category_.yml b/docs/30-modules/01-contests/02-categories/_category_.yml new file mode 100644 index 0000000..186c66a --- /dev/null +++ b/docs/30-modules/01-contests/02-categories/_category_.yml @@ -0,0 +1 @@ +label: 'Категории проектов' diff --git a/docs/30-modules/01-contests/_category_.yml b/docs/30-modules/01-contests/_category_.yml new file mode 100644 index 0000000..e106634 --- /dev/null +++ b/docs/30-modules/01-contests/_category_.yml @@ -0,0 +1 @@ +label: 'Конкурсы' diff --git a/docs/30-modules/01-intro.md b/docs/30-modules/01-intro.md new file mode 100644 index 0000000..01d57d3 --- /dev/null +++ b/docs/30-modules/01-intro.md @@ -0,0 +1,13 @@ +# Общая схема взаимодействия модулей + +```mermaid +erDiagram + Contest ||--o{ Project : "Проект участвует в конкурсе" + + Organizator ||--o{ Contest : "Организатор может создать конкурс" + Organizator ||--o{ Project : "Организатор может создать проект" + + Project ||--o{ Event: "Мероприятие проходит в рамках проекта" + Project ||--o{ News: "У проекта есть новости" + Project ||--o{ Application: "Данные проекта изменяются через заявки" +``` diff --git a/docs/30-modules/02-organizators/_category_.yml b/docs/30-modules/02-organizators/_category_.yml new file mode 100644 index 0000000..141cf43 --- /dev/null +++ b/docs/30-modules/02-organizators/_category_.yml @@ -0,0 +1 @@ +label: 'Организации и волонтеры' diff --git a/docs/30-modules/02-organizators/organizators.md b/docs/30-modules/02-organizators/organizators.md new file mode 100644 index 0000000..ba205e4 --- /dev/null +++ b/docs/30-modules/02-organizators/organizators.md @@ -0,0 +1,26 @@ +# Организаторы проектов + +```mermaid +classDiagram + class Organizator { + <> + id: pk + type: "Organization" | "Volunteers" + title: string + leader: OrgParticipant + } + + class Organization { + type: "Organization" + urData: UrData + recvezits: any + } + Organization --|> Organizator + + class Volunteers { + type: "Volunteers" + } + Volunteers --|> Organizator +``` + +- В случае волонтеров названием организатора будет являться ФИО руководителя (предварительно) diff --git a/docs/30-modules/05-applications/01-intro.md b/docs/30-modules/05-applications/01-intro.md new file mode 100644 index 0000000..2507355 --- /dev/null +++ b/docs/30-modules/05-applications/01-intro.md @@ -0,0 +1,45 @@ +# Общая последовательность работы над проектом + +- Любое изменение проекта проходит через создание заявки на изменение проекта +- Изменение в проект вносится только после одобрения заявки модератором +- При создании нового проекта, проект создается со статусом `Draft`, без создания заявки +- Проект начинает участвовать в конкурсе только после одобрения заявки модератором + +```mermaid +sequenceDiagram + actor User as Пользователь + participant Application + actor Moderation as Модератор + participant Project + +Note over User, Project: Создание проекта + + User->>+Project: Создание черновика проекта + +Note over User, Project: Заявка на участие в конкурсе + + User->>Application: Заполнение заявки + Application->>Moderation: Отправка на модерацию + Moderation-->>Application: Возврат на доработку + User-->>Application: Исправление заявки + Application-->>Moderation: Повторная оправка на модерацию + Moderation->>+Project: Проект учавствует в конкурсе + +Note over User, Project: Проект профинансирован + +loop +Note over User, Project: Заявка на изменение проекта + User->>Application: Заполнение заявки + Application->>Moderation: Отправка на модерацию + Moderation-->>Application: Возврат на доработку + User-->>Application: Исправление заявки + Application-->>Moderation: Повторная оправка на модерацию + Moderation->>+Project: Применение изменений на проект +end + + User->Project: Завершение работы над проектом + + deactivate Project + deactivate Project + deactivate Project +``` diff --git a/docs/30-modules/05-applications/02-states.md b/docs/30-modules/05-applications/02-states.md new file mode 100644 index 0000000..b496371 --- /dev/null +++ b/docs/30-modules/05-applications/02-states.md @@ -0,0 +1,72 @@ +# Спецификация + +## Исходные требования + +- Все изменения в проект (не черновик) вносятся только после прохождения модерации +- Вся история изменения проекта должна храниться столько же, сколько и сам проект + +## Проект + +```mermaid +stateDiagram-v2 + [*] --> Draft + Draft --> Rejected : Заявка отклонена модераторами + Draft --> Accepted : Заявка одобрена модераторами + Accepted --> Evaluating + Evaluating --> Awarding + + note left of Evaluating + Оценка проекта + end note + + Awarding --> Finalists : Проект не получил финансирование + Awarding --> Funded : Проект победил + + note left of Awarding + Выбор победителей + end note + + Rejected --> [*] + Finalists --> [*] + Funded --> [*] +``` + +- Проект со статусами `Draft` и `Rejected` не участвует в конкурсе +- Проект со статусами `Draft` и `Rejected` не видны никому, кроме участников организации и + модераторов +- Проект с остальными статусами участвует в конкурсе +- Отклоненный проект не участвует в конкурсе и не может быть восстановлен, только создан новый + проект +- Проект может быть удален авторами в любой любой момент до завершения конкурса без указания + причины, за исключением статуса `Funded`, т.к. этот проект уже направляется на финансирование + (не показано на схеме) +- Проект может быть отклонен организаторами конкурса в любой любой момент до завершения конкурса с + указанием причины отклонения (не показано на схеме, это техническая возможность, для этого + должны быть веские основания) + +## Заявка + +```mermaid +stateDiagram-v2 + [*] --> Draft + + Draft --> Moderating: Отправка на модерацию + + Moderating --> Rejected: Отклонена модераторами + Rejected --> [*] + + Moderating --> Accepted: Одобрена модераторами + Accepted --> [*] + + Moderating --> Returned: Возврат на доработку + Returned --> Draft: Исправление заявки + + Draft --> Deleted: Удаление заявки автором + Deleted --> [*] +``` + +- У проекта одновременно может быть несколько заявок в работе +- У проекта со статусом НЕ `Draft` обязательно должна быть заявка со статусом `Accepted`, притом + только одна +- Заявка со статусами `Draft`, `Rejected`, `Deleted` не видна никому, кроме участников организации + и модераторов diff --git a/docs/30-modules/05-applications/03-revisions.md b/docs/30-modules/05-applications/03-revisions.md new file mode 100644 index 0000000..431d788 --- /dev/null +++ b/docs/30-modules/05-applications/03-revisions.md @@ -0,0 +1,27 @@ +# Ревизии заявки + +Сама ревизия хранит только отличия от предыдущей ревизии, кто и когда внес изменения. При +необходимости можно просмотреть историю изменений заявки и откатиться к предыдущим версиям. + +## Исходные требования + +- Все изменения изменения заявки должны быть сохранены +- Исключить потерю данных при редактировании заявки несколькими пользователями +- Возможность отката к предыдущим версиям заявки +- Возможность просмотра истории изменений заявки +- Возможность формирования графика интенсивности работы над заявкой + +## Реализация + +Над заявкой могут производится различные действия, которые влияют на ее состояние и данные. При этом +разные действия могут вносить разные изменения в данные заявки. Потому в ревизии есть отдельные поля +`action` и `payloadType`, которые позволяют определить тип действия и тип изменений в данных заявки. + +Тип данных в поле `payload` зависит от значения поля `payloadType` и должно обрабатываться +соответствующим образом. + +Такая структура позволяет легко добавлять новые типы действий и изменений в заявке. + +## Заявка + +Для получения актуальной версии заявки необходимо применить все ревизии в порядке их создания. diff --git a/docs/30-modules/05-applications/_category_.yml b/docs/30-modules/05-applications/_category_.yml new file mode 100644 index 0000000..4c90024 --- /dev/null +++ b/docs/30-modules/05-applications/_category_.yml @@ -0,0 +1 @@ +label: 'Проекты и заявки' diff --git a/docs/30-modules/06-requests/_category_.yml b/docs/30-modules/06-requests/_category_.yml new file mode 100644 index 0000000..32892b7 --- /dev/null +++ b/docs/30-modules/06-requests/_category_.yml @@ -0,0 +1 @@ +label: 'Заявки (Requests)' diff --git a/docs/30-modules/06-requests/participants.md b/docs/30-modules/06-requests/participants.md new file mode 100644 index 0000000..e12071f --- /dev/null +++ b/docs/30-modules/06-requests/participants.md @@ -0,0 +1,99 @@ +# Участники проектов + +```mermaid +classDiagram + class User { + id + } + + class Contest { + id: pk + status: ContestStatus + title: string + } + + class Organizator { + <> + id: pk + type: Organization | Volunteers + title: string + } + + class Project { + id: pk + title: string + contest: Contest + org: Organizator + } + Project "*" --* "1" Organizator + Project "*" --* "1" Contest + + class ContestMember { + id: pk + contest: Contest + role: ContestRole + roleTitle: string + } + ContestMember "*" --* "1" Contest + ContestMember "*" --o "1" User + + class OrgMember { + id: pk + org: Organizator + role: OrgRole + roleTitle: string + } + OrgMember "*" --* "1" Organizator + OrgMember "*" --o "1" User + + class ProjectMember { + id: pk + project: Project + role: ProjectRole + roleTitle: string + } + ProjectMember "*" --* "1" Project + ProjectMember "*" --o "1" User + + class Participant { + id: pk + fio + phone + birthYear + user?: User + } + Participant "1" --o "0..1" User + + class OrgParticipant { + id: pk + org: Organizator + participant: Participant + roleTitle: string + } + OrgParticipant "*" --* "1" Organizator + OrgParticipant "*" --* "1" Participant + + class ProjectParticipant { + id: pk + project: Project + participant: Participant + roleTitle: string + } + ProjectParticipant "*" --* "1" Project + ProjectParticipant "*" --* "1" Participant +``` + +- Заявка превращается в конкурс (предположительно) после апрува модератором + +### Различные типы участников + +| Поле | Имя | Ключевые (руководитель, бухгалтер) | Участник проекта организации | Участник проекта волонтеров | +| ------------------------------ | -------------- | ---------------------------------- | ---------------------------- | --------------------------- | +| Фамилия | firstName | + | + | + | +| Имя | lastName | + | + | + | +| Отчество | patronymic | + | + | + | +| Должность | position | + | + | - | +| Телефон | phone | + | - | + | +| E-mail | email | + | - | + | +| Год рождения | birthYear | - | + | - | +| Зона ответственности в проекте | responsibility | - | + | - | diff --git a/docs/30-modules/06-requests/projects.md b/docs/30-modules/06-requests/projects.md new file mode 100644 index 0000000..4dcb6f3 --- /dev/null +++ b/docs/30-modules/06-requests/projects.md @@ -0,0 +1,98 @@ +# Конкурсы, проекты и заявки + +```mermaid +classDiagram + direction BT + class Organizator { + <> + id: pk + type: Organization | Volunteers + title: string + } + + class Contest { + id: pk + status: ContestStatus + title: string + logo: string + description: string + totalBudget: number + receiptStartedAt: number + receiptEndedAt: number + ratingStartedAt?: number + ratingEndedAt?: number + publicResultsAt: number + workStartedAt: number + workEndedAt: number + createdAt: number + updatedAt: number + org: Organizator + } + Contest "*" --* "1" Organizator + + class Activity { + id: pk + title: string + parent?: Activity + } + Activity "*" --* "1" Contest + Activity "1" --* "0..1" Activity + + class Project { + id: pk + title: string + contest: Contest + activity: Activity + org: Organizator + leader: ProjectParticipant + description: string + } + Project "*" --* "1" Organizator + Project "*" --* "1" Contest + Project "*" --o "1" Activity + + class OrgParticipant { + id: pk + org: Organizator + fio + roleTitle: string + } + OrgParticipant "*" --* "1" Organizator + + class ProjectParticipant { + id: pk + project: Project + fio + roleTitle: string + } + ProjectParticipant "*" --* "1" Project + + class Event { + id: pk + status: EventStatus + project: Project + title: string + logo + description: string + startsOn: Date + endsOn: Date + createdAt: timestamp + updatedAt: timestamp + } + Event "*" --* "1" Project + + class News { + id + status: EventStatus + project: Project + title: string + logo + description: string + date: Date + createdAt: timestamp + updatedAt: timestamp + } + News "*" --* "1" Project +``` + +- Заявка превращается в конкурс (предположительно) после апрува модератором diff --git a/docs/30-modules/06-requests/requests.md b/docs/30-modules/06-requests/requests.md new file mode 100644 index 0000000..11117cf --- /dev/null +++ b/docs/30-modules/06-requests/requests.md @@ -0,0 +1,24 @@ +# Заявки + +## Состояния заявки + +```mermaid +stateDiagram-v2 + [*] --> Draft + Draft --> Autosave + Autosave --> Draft: Обновление черновика + Draft --> Moderating: Отправлено на модерацию + Moderating --> Returned: на доработку + Returned --> Draft: исправление + Moderating --> Rejected: заявка отклонена + Moderating --> Accepted: заявка принята + Accepted --> Archived: обновление заявки принято + Rejected --> [*] +``` + +- `Autosave` отдельная запись с указанием на черновик, при сохранении обновляет черновик +- `Accepted` и `Archived` обязаны иметь верное значение `projectId` т.к. при принятии заявки + создается проект и дальнейшие действия ведутся над проектом +- Заявки в статусе отличном от `Accepted` и `Archived` могут иметь `projectId` только если это + заявка на обновление проекта +- Отклоненная заявка не может быть подана повторно diff --git a/docs/30-modules/09-evaluation/01-overview.md b/docs/30-modules/09-evaluation/01-overview.md new file mode 100644 index 0000000..9c2c957 --- /dev/null +++ b/docs/30-modules/09-evaluation/01-overview.md @@ -0,0 +1,17 @@ +# Введение + +Данный модуль обеспечивает экспертную оценку заявок. Он автоматизирует распределение заявок между +экспертами, сбор и анализ оценок, а также формирование итогового рейтинга проектов. + +## Общий процесс оценки проектов + +1. Организатор настраивает форму оценки и критерии. +2. Система назначает экспертов на проекты. +3. Эксперты заполняют форму оценки. +4. Итоговые оценки агрегируются для формирования рейтинга. + +## Роли участников + +- **Организатор конкурса** – настраивает критерии, назначает экспертов, контролирует процесс. +- **Эксперт** – оценивает проекты по заданным критериям. +- **Платформа** – автоматически распределяет проекты, фиксирует оценки, собирает данные. diff --git a/docs/30-modules/09-evaluation/02-evaluation-form.md b/docs/30-modules/09-evaluation/02-evaluation-form.md new file mode 100644 index 0000000..5a3068a --- /dev/null +++ b/docs/30-modules/09-evaluation/02-evaluation-form.md @@ -0,0 +1,20 @@ +# Создание формы оценки + +## Назначение и общие принципы + +Так как эксперты оценивают проекты по заранее настроенным критериям, требуется конструктор формы, +которую будут заполнять эксперты. Организатор конкурса настраивает критерии оценки до начала +конкурса. После начала оценки структура формы не может быть изменена. + +## Типы критериев + +- **Выбор из фиксированного списка ответов** (каждый вариант имеет скрытый для эксперта вес). +- **Текстовое поле** с настройками ограничений по длине. + +Так же критерию можно добавить описание и подсказку. + +## Группировка критериев + +- Критерии разделены на группы. +- Некоторые критерии могут быть необязательными. +- Если для группы критериев требуется комментарий, то его нужно добавить в схему. diff --git a/docs/30-modules/09-evaluation/03-expert-assignment.md b/docs/30-modules/09-evaluation/03-expert-assignment.md new file mode 100644 index 0000000..02fedff --- /dev/null +++ b/docs/30-modules/09-evaluation/03-expert-assignment.md @@ -0,0 +1,36 @@ +# Назначение экспертов + +## Принципы назначения + +Платформа для грантов должна распределять проекты среди экспертов для независимой оценки. Процесс +назначения проектов должен учитывать: + +- Автоматическое и ручное распределение. +- Обеспечение равномерной нагрузки на экспертов. +- Возможность перераспределения проектов в случае отказа эксперта. + +## Ручное назначение + +Организатор может вручную назначать экспертов и корректировать автоматическое распределение. + +## Автоматическое назначение + +Система может: + +- Находить и назначать на проект наименее загруженного эксперта. +- Балансировать указанный проект. +- Балансировать все проекты категории. + +Балансировать проект - доназначать экспертов до нужного количества, если сейчас их меньше, чем надо. + +Обработка ошибок: + +- Если найти и назначить наименее загруженного **эксперта** не удаётся, пользователю предлагается + **назначить его вручную**. +- Если для балансировки **проекта** не хватает экспертов, операция **отменяется**. +- Если для балансировки какого-то из проектов **категории** не хватает экспертов, этот **проект + помечается** `unassessable` и балансировка категории продолжается. + +## Обработка отказов + +Если эксперт отказывается от оценки, система автоматически переназначает проект другому эксперту. diff --git a/docs/30-modules/09-evaluation/04-project-evaluation.md b/docs/30-modules/09-evaluation/04-project-evaluation.md new file mode 100644 index 0000000..c08f726 --- /dev/null +++ b/docs/30-modules/09-evaluation/04-project-evaluation.md @@ -0,0 +1,12 @@ +# Оценка проектов экспертами + +## Процесс оценки + +- Эксперт оценивает проект независимо, не видя оценок других экспертов. +- Черновики сохраняются автоматически. +- Эксперт может редактировать оценку, пока она не отправлена. +- После отправки оценку изменить нельзя. + +## Форма + +- Обязательное заполнение всех критериев. diff --git a/docs/30-modules/09-evaluation/05-review-analysis.md b/docs/30-modules/09-evaluation/05-review-analysis.md new file mode 100644 index 0000000..cac9dc4 --- /dev/null +++ b/docs/30-modules/09-evaluation/05-review-analysis.md @@ -0,0 +1,60 @@ +# Анализ ревью + +После того как эксперты отправляют свои оценки, система автоматически анализирует их, вычисляет +средний балл проекта и определяет, завершена ли экспертиза. + +Это происходит при каждом изменении ревью: при завершении, отклонении или отказе от ревью. + +## Средний балл + +Средний балл проекта рассчитывается как среднее арифметическое баллов всех завершённых ревью, +округлённое до целого. Незавершённые, отклонённые и удалённые ревью в расчёте не участвуют. Если ни +одно ревью ещё не завершено, средний балл не отображается. + +## Завершение экспертизы + +Экспертиза проекта считается завершённой, когда выполнены два условия одновременно: + +- Все назначенные эксперты завершили свои ревью (не считая отклонённых и удалённых). +- Количество завершённых ревью не меньше минимально необходимого количества, заданного в + настройках номинации. + +## Спорные оценки + +Когда эксперты расходятся во мнениях слишком сильно, система помечает проект как спорный. Это +позволяет организатору обратить внимание на такие проекты и при необходимости назначить +дополнительных экспертов. + +### Как определяется спорность + +Проверка спорности происходит, когда набрано ровно минимально необходимое количество ревью. Система +сравнивает разброс оценок — разницу между максимальным и минимальным баллом среди всех завершённых +ревью — с допустимым порогом, заданным в настройках номинации в процентах от шкалы оценки. + +- Если разброс превышает порог — проект помечается как спорный и экспертиза не завершается, чтобы + организатор мог назначить дополнительного эксперта. +- Если разброс в пределах порога — проект не спорный, экспертиза завершается. + +### Дополнительные ревью + +Когда организатор назначает дополнительного эксперта на спорный проект и тот завершает ревью, +количество ревью превышает минимально необходимое. В этом случае: + +- Проект остаётся помеченным как спорный (метка не снимается автоматически). +- Экспертиза завершается, когда все назначенные эксперты завершили свои ревью. +- Модератор должен вручную снять метку спорности, если считает, что дополнительное ревью разрешило + спор. + +### При недостатке ревью + +Если количество завершённых ревью меньше минимально необходимого (например, ревью было отклонено), +метка спорности снимается и экспертиза остаётся незавершённой. + +## Пересчёт проектов всей номинации + +При изменении настроек оценки в номинации запускается пересчёт всех проектов в номинации. + +При пересчёте: + +- Затрагиваются только проекты с незавершённой или завершённой экспертизой. +- Метка спорности сбрасывается у всех проектов и вычисляется заново. diff --git a/docs/30-modules/09-evaluation/_category_.yml b/docs/30-modules/09-evaluation/_category_.yml new file mode 100644 index 0000000..5df0147 --- /dev/null +++ b/docs/30-modules/09-evaluation/_category_.yml @@ -0,0 +1 @@ +label: 'Оценка проектов' diff --git a/docs/30-modules/10-statements/_category_.yml b/docs/30-modules/10-statements/_category_.yml new file mode 100644 index 0000000..cee4d55 --- /dev/null +++ b/docs/30-modules/10-statements/_category_.yml @@ -0,0 +1 @@ +label: 'Отчетность' diff --git a/docs/30-modules/10-statements/intro.md b/docs/30-modules/10-statements/intro.md new file mode 100644 index 0000000..816e02e --- /dev/null +++ b/docs/30-modules/10-statements/intro.md @@ -0,0 +1,11 @@ +# Отчетность + +Пользователи, получившие гранты должны предоставить отчеты по расходу полученных средств + +Отчетности всего две + +## Финансовый отчет + +Сколько было потрачено средств, на что, с комментариями и прикреплением документов + +## Аналитический отчет diff --git a/docs/30-modules/_category_.yml b/docs/30-modules/_category_.yml new file mode 100644 index 0000000..1d181a1 --- /dev/null +++ b/docs/30-modules/_category_.yml @@ -0,0 +1 @@ +label: 'Продуктовые модули' diff --git a/docs/40-guidelines/_category_.yml b/docs/40-guidelines/_category_.yml new file mode 100644 index 0000000..8d94b1b --- /dev/null +++ b/docs/40-guidelines/_category_.yml @@ -0,0 +1 @@ +label: 'Гайдлайны' diff --git a/docs/40-guidelines/modal.md b/docs/40-guidelines/modal.md new file mode 100644 index 0000000..7cd5dd7 --- /dev/null +++ b/docs/40-guidelines/modal.md @@ -0,0 +1,202 @@ +# Модальное окно + +Модальное окно — диалоговый элемент интерфейса, который появляется поверх страницы и блокирует +доступ к её основному содержимому. + +## Когда использовать + +Используйте модальные окна для: + +- подтверждения действий, +- отображения ошибок, +- вывода небольших форм (до 10 полей), связанных с локальными действиями — например, настройками + или созданием объекта. + +Не используйте модальные окна для больших форм (> 15 полей) и действий, требующих длительного +непрерывного взаимодействия пользователя с интерфейсом. + +## Принцип работы + +Модальное окно отображается поверх страницы с затемнением фона. Это помогает сфокусировать внимание +пользователя на локальном действии, сохранив контекст. + +Модальное окно: + +- перехватывает фокус внутри себя, +- восстанавливает фокус после закрытия (свойство `restoreFocus`). + +## Состав модального окна + +Модальное окно может включать: + +- **заголовок** (обязателен), +- **кнопку закрытия**, +- **иконку статуса**, +- **контент**, +- **подвал с действиями**. + +## Компоненты модальных окон + +### `NoticeDialog` + +Используется для отображения информационных сообщений: + +- успешные действия, +- ошибки, +- предупреждения, +- служебные уведомления. + +Особенности: + +- одна кнопка действия («ОК» или «Закрыть»), +- иконка и цвет определяют тип сообщения, +- контент — опционален, +- кнопка получает фокус. + +Типы сообщений: + +- успех — зелёное оформление `variant=success` и позитивная иконка, +- ошибка — красное оформление `variant=danger` и иконка ошибки, +- предупреждение — жёлтое оформление `variant=warning` и иконка предупреждения, +- информация — синее оформление `variant=info` и информационная иконка. + +### `ActionDialog` + +Используется для модальных окон, которые последовательно проходят следующие состояния: + +- подтверждение действия, +- выполнение асинхронной операции, +- отображение статуса выполнения, +- сообщение об успехе или ошибке. + +Особенности: + +- объединяет несколько состояний в одном окне, +- обеспечивает единый UX-паттерн для типовых async-сценариев, +- используется вместо самостоятельной реализации поведения, если сценарий вписывается в типовой + flow. + +### `Dialog` + +Базовый компонент модального окна. Предназначен для: + +- подтверждения действий, +- ввода дополнительной информации, +- отображения сценариев, не охваченных `NoticeDialog` и `ActionDialog`. + +Особенности: + +- может содержать любое количество кнопок, +- может быть гибко настроен под различные сценарии, +- требует самостоятельной реализации логики поведения. + +## Диалоговые окна + +### Заголовок + +Заголовок должен быть кратким (1–3 слова) и отражать суть действия или процесса: + +- Создание проекта +- Редактирование события + +Для окон, требующих подтверждения: + +- Удалить проект? +- Выйти без сохранения? + +### Действия + +В диалоговых окнах обычно две кнопки: + +- **Основная кнопка** — подтверждает действие («Сохранить», «Удалить»). Основная кнопка должна + быть в фокусе. +- **Кнопка отмены** — закрывает окно без выполнения действия («Отменить»). + +> **Правило:** чем правее кнопка — тем менее важное и менее частое действие. Вот обновлённая краткая +> версия с переносом правил подтверждения в конец: + +## Именование компонентов + +Для модальных окон используется единый принцип именования, который отражает тип действия и +обеспечивает предсказуемость. + +### Обычные модальные окна + +Используется паттерн: + +> **`Modal`** + +Применяется для создания, редактирования, просмотра или выполнения безопасных действий. + +Где: + +- **Entity** — сущность: `Project`, `Review`, `Expert`, … +- **Action** — действие в глагольной форме: `Create`, `Edit`, `Assign`, `View`, … + +Примеры: + +- `ProjectCreateModal` +- `CategoryEditModal` +- `ReviewDetailsModal` +- `ExpertAssignModal` + +### Подтверждение действий + +Для модальных окон, которые подтверждают действие, добавляется префикс: + +> **`ConfirmModal`** + +Примеры: + +- `ProjectDeleteConfirmModal` +- `ExpertUnassignConfirmModal` +- `ReviewRejectConfirmModal` + +### Преимущества схемы + +- структурированность, +- предсказуемость, +- понятные правила, +- единообразие в кодовой базе. + +## Закрытие модального окна + +Клик по иконке закрытия, нажатие _Esc_ или клик вне модального окна — **эквивалентные действия**, +приводящие к отмене и закрытию. + +Исключения: + +- важные процессы, которые нельзя прервать случайно; +- формы ввода данных, во избежание потери изменений, если эти изменения критичны. + +В таких случаях: + +- окно может не закрываться через Esc/клик вне области; +- перед закрытием нужно уточнить: сохранить изменения или выйти. + +## Асинхронные действия + +Используйте `ActionDialog`, если: + +- нужно подтвердить действие, +- выполнить асинхронную операцию, +- показать прогресс, +- отобразить успех или ошибку. + +Для особых сценариев допускается использовать обычный `Dialog`, но: + +- во время выполнения действия кнопки должны быть заблокированы, +- окно не должно закрываться до завершения операции, +- ошибка должна быть отображена в понятной форме. + +## Dialog vs Modal + +> Кратко: **Dialog — технология, Modal — сценарий продукта.** + +- **`Dialog`** используется только для инфраструктурных UI-компонентов дизайн-системы (например, + `Dialog`, `ActionDialog`, `NoticeDialog`). Это строительные блоки, не связанные с доменными + сущностями. + +- **`Modal`** используется для прикладных модальных окон, связанных с реальными пользовательскими + сценариями (`ProjectCreateModal`, `ConfirmProjectDeleteModal`, `ReviewDetailsModal`). Такие + компоненты используют `Dialog` внутри, но представляют собой доменные элементы интерфейса. diff --git a/docs/50-developing/01-quick-start.md b/docs/50-developing/01-quick-start.md new file mode 100644 index 0000000..8a2d314 --- /dev/null +++ b/docs/50-developing/01-quick-start.md @@ -0,0 +1,61 @@ +# Быстрый старт + +## Репозиторий + +Настраиваем [ssh ключи в gitea](https://git.jt4d.ru/user/settings/keys), клонируем репозиторий и +ставим зависимости. В проекте используется пакетный менеджер yarn + +```bash +git clone ssh://git@git.jt4d.ru:2222/1vit/more.git +cd more +yarn +``` + +Установка может занять несколько минут. + +## Настройка БД + +Рекомендуется устанавливать всё через docker чтобы избежать проблем с зависимостями и сделать +окружение более повторяемым. + +Клонируем [репозиторий с docker-compose](https://git.jt4d.ru/1vit/pgsql) и выполняем +`docker compose up`. Если команда не найдена, ставим compose +[пакетом](https://docs.docker.com/compose/install/linux/#install-using-the-repository) или +[из исходников](https://docs.docker.com/compose/install/linux/#install-the-plugin-manually). + +Потом создаём в корне проекта (основного репозитория) файл `.env` (пример в `.env.example`). Он +используется для подключения к основной БД. + +``` +DB_HOST=localhost +DB_PORT=5432 +DB_USERNAME=1vit_more +DB_PASSWORD=password +DB_DATABASE=1vit_more +``` + +Ещё рекомендуется создать `.env.test` (пример в `.env.test.example`), он будет использоваться для +тестов с использованием БД. Если файл не найден, будет перезаписываться основная база. + +Проверяем подключение `yarn db:show` и накатываем миграции (создание структуры данных, schema) +`yarn db:migrate`. + +Далее нужно заполнить БД демо-данными при помощи команды `yarn test-data:db-restore`. + +## Запуск + +| тип сервера | команда | +| ------------------------------------------------- | ------------------------------------------------------------------ | +| Полный dev сервер с БД | `yarn start` | +| Сторибук (для тестирования отдельных компонентов) | `yarn storybook` | +| Jest (unit-тесты) | `yarn test:unit` или `yarn test:unit path/to/file` | +| Jest (db-тесты) | `yarn test:db`, см. [доку про дб тесты](/docs/developing/database) | + +## CI + +При каждом `git push` запускается сборка приложения на +[Gitea Actions](https://git.jt4d.ru/1vit/more/actions). + +Стенд для ветки доступен по адресу `http://.`. + +Подробнее о системе деплоя, тегах и продакшн-деплое — в [разделе «Поставка»](/docs/deploy/ci-cd). diff --git a/docs/50-developing/02-branches.md b/docs/50-developing/02-branches.md new file mode 100644 index 0000000..05aba42 --- /dev/null +++ b/docs/50-developing/02-branches.md @@ -0,0 +1,58 @@ +# Ветки и пулл-реквесты + +## Branches + +Для работы над задачей создаётся ветка с названием формата `i<номер_issue>-<краткое_описание>`. +Краткое описание это БУКВАЛЬНО 1-2 слова. Примеры: + +```txt +i472-statements +i343-project-page +``` + +## Pull requests + +Сразу после загрузки (`git push`) ветки рекомендуется создавать ей pull request и привязать его к +задаче + +Описание PR должно иметь вид список изменений - пустая строка - вид отношения к задаче и +\#номер_задачи, пример: + +```md +- cover `viewRoot` flag in `users.dal` with db tests +- add `viewRole.findOne` test group + +Closes #381 +``` + +или + +```md +- добавил тесты на наличие буквы, цифры, спецсимвола и на повторяемость. +- улучшен алгоритм генерации пароля + +Closes #433 +``` + +или + +```md +- rename statements-period.types +- fix DateView stories + +Related to #472 +``` + +1. Загружаем (`git push`) ветку +2. Создаём pull request с названием `WIP: <название_ветки>` +3. Добавляем описание +4. Добавляем PR в зависимости задачи (Dependencies справа) + +Когда ветка готова, отправляем на ревью (Reviewers справа), убираем префикс `WIP` из названия и +перемещаем в столбец `To review` на [доске](http://git.arswarog.ru/1vit/more/projects/1). + +## Доска проекта + +Статус задач можно отслеживать на [доске проекта](http://git.arswarog.ru/1vit/more/projects/1). Pull +request на доску не добавляем, там должны быть только задачи. PR будет отображаться под задачей если +его добавить в зависимости. diff --git a/docs/50-developing/03-files.md b/docs/50-developing/03-files.md new file mode 100644 index 0000000..4e81821 --- /dev/null +++ b/docs/50-developing/03-files.md @@ -0,0 +1,88 @@ +# Файловая структура проекта + +## Фронтенд + +### Гостевой фронтенд + +Располагается в `/src/visitor` + +- Лендинг на главной странице +- Страница организатора +- Страница проекта + +Все они имеют уникальный дизайн (на `scss`), и могут быть быстро изменены + +#### Ограничения + +Может обращаться только к `common` + +### Клиентский фронтенд + +Располагается в `/src/client` + +- регистрация +- авторизация +- личный кабинет + - создание заявок +- панель эксперта +- панель модератора +- панель админа + +#### Ограничения + +Может обращаться только к `common` + +--- + +## Бэкенд + +Располагается в папке `/src/server` + +### Ограничения + +Может обращаться только к `common` и `client` + +--- + +## Общие типы и функции + +Располагаются в папке `/src/common` + +### Ограничения + +Не может обращаться ни к каким другим частям, т.к. является корнем + +## Демо данные + +Располагаются в папке `/src/common` + +Требования: + +- Файл с этими данными должен называться `<название>.demo.ts`, где `<название>` - это название + типа во множественном числе +- Переменная с демо данными должна начинаться с `demo` и иметь вид `demo<название>` +- Переменные с вариантами данных одного типа должны начинаться с `demo<название>` и иметь вид + `demo<название><название варианта>` +- Если демо данных одного типа больше одного значения, то эти значения располагаются в этом же + файле + +Рекомендация: + +В названии константы указывать её тип (`demoEvent: IEvent`). + +### Пример + +Пример организации демо данных участников: + +```ts +/// file: src/demoData/participants.ts + +export const demoParticipantBase: IBaseParticipant = { ... } + +export const demoParticipantKey: IKeyParticipant = { ... } + +export const demoParticipants: IParticipant[] = [ + demoParticipantBase, + demoParticipantKey, +] +``` diff --git a/docs/50-developing/10-modules.md b/docs/50-developing/10-modules.md new file mode 100644 index 0000000..92b8427 --- /dev/null +++ b/docs/50-developing/10-modules.md @@ -0,0 +1,58 @@ +# Схема зависимостей модулей + +```mermaid +classDiagram + +Applications + Applications <|-- Evaluation + +Auth + +Client + +Contests + Contests : ContestsService + Contests : ContestAreasService + Contests <|-- Requests + Contests <|-- Applications + Contests <|-- Evaluation + +Events + Events : EventsService + Events <|-- Requests + +note for Events "Events должен\n зависеть от Requests,\n а не наоборот" + +Expenses + Expenses : ExpensesService + Expenses <|-- Statements + +Export + Export : ExportService + Export <|-- Statements + Export <|-- Requests + +Files + Files : FilesService + +Organizators + Organizators : OrganizatorsService + Organizators <|-- Applications + Organizators <|-- Contests + Organizators <|-- Requests + +Profile + +Requests + Requests : RequestsService + Requests <|-- Expenses + Requests <|-- Statements + +Statements + Statements : StatementsPeriodsService + +Users + Users : UsersService + Users <|-- Client + Users <|-- Profile +``` diff --git a/docs/50-developing/20-database.md b/docs/50-developing/20-database.md new file mode 100644 index 0000000..609db45 --- /dev/null +++ b/docs/50-developing/20-database.md @@ -0,0 +1,110 @@ +# Тестирование с использованием реального бэкенда + +Поднимается полноценнный nest сервер, затем для каждого теста БД сбрасывается и восстанавливается из +снапшота (`/test-data/db/`) + +### CI/CD + +Для каждого прогона + +- создаётся база тестовая данных +- запускаются тесты с бэком +- тестовая база удаляется + +Для CI/CD отдельных настроек не требуется + +### Настройка + +Настройки для тестов хранятся в файле `.env.test`. Пример файла: + +```env +DB_USERNAME=test_user +DB_PASSWORD=secret +DB_DATABASE=test_db +``` + +Все недостающие значения будут взяты из `.env`. + +### Локальный запуск + +При запуске тестов можно использовать команды + +- `yarn test:db` запустит только тесты с БД +- `yarn test` запустит все тесты в проекте + +### Написание тестов + +Тесты должны иметь расширение `.db-spec.ts`. + +Для создания и запуска бэка используется утилита `createTestingBackend()`. В возвращаемом ей +интерфейсе есть: + +- метод `resetState()` - нужно запускать перед каждым тестом +- метод `destroy()` - вызывать в конце группы тестов +- поле `http` - результат вызова `INestApplication.getHttpServer()`. + +Также есть утилита `authenticate`, которую можно использовать для входа. + +Пример управления состоянием: + +```ts +let app: ITestingBackend; + +beforeAll(async () => { + app = await createTestingBackend(); +}); + +afterAll(async () => { + await app.destroy(); +}); + +beforeEach(async () => { + await app.resetState(); +}); +``` + +Пример теста: + +```ts +it('при создании AppForm имеет статус draft и не удалена', async () => { + const api = supertest(app.http); + + const response = await api + .post('/v1/contests/1/forms') + .set('Cookie', await authenticate(api, 'admin')) + .send(exampleValidForm) + .expect(201); + + expect(response.body).toMatchObject({ + draft: true, + deleted: false, + }); +}); +``` + +### Тестирование изолированной части бэкенда + +Для unit-тестов определённой части бэкенда можно использовать утилиту `createTestingHarness(app)`. + +Пример: + +```ts +const module = await Test.createTestingModule({ + providers: [UsersDal, TimeService, createMockConfigProvider(authConfigToken)], + imports: [ + TypeOrmModule.forRoot({ + ...testOrmCredentials, + entities: [User, Contest, AuthToken], + synchronize: false, + logging: false, + }), + TypeOrmModule.forFeature([User]), + ], +}).compile(); + +app = module.createNestApplication(); +await app.init(); + +const harness = await createTestingHarness(app); +harness.resetState(); // имеет тот же интерфейс, что createTestingBackend +``` diff --git a/docs/50-developing/_category_.yml b/docs/50-developing/_category_.yml new file mode 100644 index 0000000..77fa202 --- /dev/null +++ b/docs/50-developing/_category_.yml @@ -0,0 +1 @@ +label: 'Разработчику' diff --git a/docs/55-logging/01-overview.md b/docs/55-logging/01-overview.md new file mode 100644 index 0000000..f3a3e62 --- /dev/null +++ b/docs/55-logging/01-overview.md @@ -0,0 +1,67 @@ +# Введение + +## Зачем нужно логирование + +### Цель + +Логирование — это ключевой инструмент диагностики и анализа поведения системы. Оно фиксирует +**действия, ошибки и события**, позволяя: + +- анализировать поведение приложения; +- находить причины ошибок и сбоев; +- отслеживать бизнес-процессы (например, запуск конкурса или назначение эксперта); +- проводить аудит действий пользователей; +- контролировать производительность и стабильность работы. + +Хорошее логирование даёт **контекст и доказательства** — кто, когда и что сделал, с каким +результатом. + +### Основные задачи логирования + +Логирование решает как **технические**, так и **организационные** задачи. + +**Технические:** + +- **Диагностика:** восстановление хода событий при сбое. +- **Аналитика:** понимание того, как система используется. +- **Поддержка:** ускорение поиска причин ошибок в эксплуатации. + +**Организационные:** + +- **Аудит:** фиксация действий пользователей и администраторов. +- **Безопасность:** выявление попыток несанкционированного доступа. + +## Уровни логирования + +В системе используется библиотека **Pino**, интегрированная через `nestjs-pino`. Она поддерживает +стандартные уровни логирования, отражающие важность события. + +| Уровень | Метод | Когда использовать | Пример | +| --------- | --------- | --------------------------------------------------- | -------------------------------- | +| **TRACE** | `trace()` | Максимальная детализация, пошаговая трассировка | Проверка цепочки вызовов | +| **DEBUG** | `debug()` | Отладочная информация о логике работы | Вывод промежуточных данных | +| **INFO** | `log()` | Обычные события нормальной работы | Создание проекта | +| **WARN** | `warn()` | Нежелательные, но некритичные ситуации | Отказ в доступе | +| **ERROR** | `error()` | Ошибки, требующие внимания и реакции | Исключение при сохранении | +| **FATAL** | `fatal()` | Критические сбои, приводящие к остановке приложения | Потеря соединения с базой данных | + +> 💡 Все уровни логов фиксируются одинаково по структуре данных — различается только их +> **важность**. + +## Различие между dev и prod + +Логирование настроено так, чтобы быть **удобным в разработке** и **эффективным в продакшене**. + +| Среда | Формат | Уровень по умолчанию | Особенности | +| --------------- | ----------------- | -------------------- | --------------------------------------------------------------------- | +| **Development** | человеко-читаемый | `debug` | Цвета, отступы, подробные данные — удобно для чтения в консоли. | +| **Production** | JSON | `info` | Машиночитаемый формат для централизованной агрегации и анализа логов. | + +> Состав данных в логе всегда одинаков — меняется только способ отображения. Уровень логирования +> можно переопределить через переменные окружения. + +Таким образом: + +- в **разработке** акцент на удобство восприятия и диагностику; +- в **продакшене** — на структурированные данные и интеграцию с системами мониторинга (например, + Loki или ELK). diff --git a/docs/55-logging/02-using.md b/docs/55-logging/02-using.md new file mode 100644 index 0000000..0f12698 --- /dev/null +++ b/docs/55-logging/02-using.md @@ -0,0 +1,91 @@ +# Подключение и использование + +## Получение логгера + +Логгер внедряется в любой сервис, контроллер или компонент через **dependency injection**. После +внедрения требуется указать **контекст**, чтобы логи было проще анализировать. + +```ts +@Injectable() +export class ProjectService { + constructor(private readonly logger: Logger) { + this.logger.setContext('Projects'); + } + + async createProject(data: CreateProjectDto) { + this.logger.log('Project created', { project: 42, user: data.ownerId }); + } +} +``` + +## Контекст логгера + +Контекст указывает, **к какому модулю или бизнес-процессу относится лог**. Он помогает группировать +события и быстрее понимать источник при анализе системы. + +Контекст задаётся вручную при инициализации логгера: + +```ts +constructor(private readonly logger: Logger) { + this.logger.setContext('Evaluation'); +} +``` + +## Правила именования контекста + +Хорошие контексты делают логи самодокументируемыми. Чтобы они оставались единообразными, следуйте +правилам: + +1. **Основой контекста является название модуля**, отражающее область бизнес-логики. Примеры: + + - `Evaluation` — процесс оценки заявок; + - `Contests` — управление конкурсами; + - `Users` — работа с пользователями. + +2. **При необходимости уточнения** добавляется второй уровень через двоеточие: + + - `Evaluation:ExpertAssignment` — назначение экспертов; + - `Applications:Moderation` — модерация проектов. + +3. **Технические роли** (`Service`, `Controller` и т. п.) в названии **не используются**, так как + контекст должен отражать **бизнес-смысл**, а не структуру кода. + +> 💬 Контекст — часть “языка логирования” проекта. Он должен быть **стабильным, понятным и +> единообразным** во всех модулях. + +## Контекст запроса + +При логировании внутри HTTP-запроса система автоматически добавляет поля: + +- `user` — идентификатор пользователя (если авторизован); +- `url` — адрес текущего запроса. + +Это позволяет связывать бизнес-события с конкретными действиями пользователей и упрощает анализ +логов. + +## Переменные окружения + +Настройки логирования задаются через `.env` или переменные окружения. + +| Переменная | Назначение | Пример значения | +| ----------------- | -------------------------------------------------------- | -------------------------------- | +| `LOG_LEVEL` | Минимальный уровень логирования. | `debug`, `info`, `warn`, `error` | +| `LOG_PRETTY` | Включает человеко-читаемый формат (обычно в dev). | `true` | +| `LOG_SILENT_HTTP` | Отключает автоматические HTTP-логи (актуально в тестах). | `true` | + +Если переменные не заданы, применяются значения по умолчанию: + +| Окружение | Значения по умолчанию | +| --------- | ------------------------------------ | +| **dev** | `LOG_LEVEL=debug`, `LOG_PRETTY=true` | +| **prod** | `LOG_LEVEL=info`, `LOG_PRETTY=false` | + +> 🔧 Уровень логирования можно переопределить без релиза — через переменные окружения при запуске +> приложения. + +## Чек-лист перед ревью + +- [ ] Логгер подключён через dependency injection. +- [ ] Контекст задан явно через `setContext()`. +- [ ] Уровень логирования соответствует окружению. +- [ ] Логи не содержат чувствительных данных. diff --git a/docs/55-logging/03-auto-logging.md b/docs/55-logging/03-auto-logging.md new file mode 100644 index 0000000..223aacc --- /dev/null +++ b/docs/55-logging/03-auto-logging.md @@ -0,0 +1,106 @@ +# Автоматическое логирование + +Автоматическое логирование фиксирует системные события **без участия разработчика**. Оно +обеспечивает единый формат сообщений и полное покрытие ключевых операций платформы — +**HTTP-запросов**, **проверок доступа (RBAC)** и **ошибок**. + +## Назначение + +Цели автоматического логирования: + +- Сократить количество ручных логов в коде. +- Обеспечить единообразие сообщений и уровней логирования. +- Повысить наблюдаемость и удобство диагностики. +- Позволить анализировать производительность и отказы без изменения бизнес-логики. + +> 💡 Автоматические логи всегда присутствуют и формируются системой независимо от действий +> разработчика. + +## Источники автоматических логов + +| Тип события | Источник | Что логируется | +| --------------------------- | ---------------------------- | -------------------------------------------------------------- | +| **HTTP-запросы** | middleware `pino-http` | Каждый завершённый запрос с кодом ответа и временем обработки. | +| **Проверки доступа (RBAC)** | модуль `RbacModule` | Успешные и неуспешные проверки разрешений. | +| **Ошибки и исключения** | глобальный `ExceptionFilter` | Необработанные ошибки приложения и системные сбои. | + +Все эти события регистрируются автоматически и не требуют дополнительного кода в модулях. + +## HTTP-запросы + +### Общие принципы + +- Каждый HTTP-запрос логируется **после завершения обработки**. +- Лог формируется middleware `pino-http` на основе данных ответа. +- Разработчику не нужно добавлять логи вручную — они создаются автоматически. + +Каждая запись содержит: + +- HTTP-метод (`GET`, `POST`, `PATCH` и т. д.); +- путь запроса (`/api/v1/...`); +- код ответа (`200`, `403`, `500` и т. д.); +- время выполнения (в миллисекундах); +- идентификатор пользователя (`user`, если известен). + +### Уровни логирования + +| Категория ответа | Пример кода | Уровень | +| ---------------- | ----------- | ------- | +| Успешный ответ | `2xx` | `info` | +| Ошибка клиента | `4xx` | `info` | +| Ошибка сервера | `5xx` | `error` | + +> Уровень `warn` для HTTP-логов не используется — это предотвращает появление ложных предупреждений +> при корректных отказах. + +### Задачи HTTP-логов + +- Отслеживание активности API. +- Измерение времени обработки запросов. +- Анализ причин ошибок и неудачных вызовов. +- Связь действий пользователей с конкретными маршрутами. + +## Проверки доступа (RBAC) + +### Общие принципы + +Все проверки доступа логируются **автоматически** при вызовах методов `check*` и `accessDenied`. Это +обеспечивает аудит всех случаев — и разрешённых, и запрещённых действий. + +### Уровни логирования + +| Событие | Уровень | Что фиксируется | +| --------------- | ------- | -------------------------------------------------- | +| Отказ в доступе | `warn` | Попытка выполнить действие без нужного разрешения. | +| Доступ разрешён | `debug` | Успешная проверка разрешения. | + +> Такое распределение уровней помогает разделять реальные проблемы (отказы) и штатные сценарии +> (успешные проверки). + +## Ошибки и исключения + +### Общие принципы + +Глобальный `ExceptionFilter` автоматически логирует все необработанные ошибки: + +- `error` — ошибки приложения (например, неверные данные или сбой бизнес-логики); +- `fatal` — критические сбои, приводящие к остановке приложения (например, невозможность + подключиться к базе данных). + +### Структура записи + +Каждая запись об ошибке включает: + +- `trace` — стек вызовов; +- `context` — модуль, где произошла ошибка; +- `user` и `url` — если ошибка возникла в контексте HTTP-запроса. + +> Это обеспечивает точную локализацию проблем и упрощает разбор инцидентов. + +## Рекомендации + +- Не дублируйте автоматические логи вручную. +- Если требуется зафиксировать **дополнительный контекст**, добавляйте отдельный лог рядом с + бизнес-событием — но не повторяйте уже зарегистрированные HTTP-запросы или проверки доступа. +- При анализе проблем **начинайте с автоматических логов** — они формируются всегда и имеют единый + формат. diff --git a/docs/55-logging/04-manual-logs.md b/docs/55-logging/04-manual-logs.md new file mode 100644 index 0000000..5d00444 --- /dev/null +++ b/docs/55-logging/04-manual-logs.md @@ -0,0 +1,123 @@ +# Ручное логирование и бизнес-события + +Автоматическое логирование фиксирует системные события (HTTP-запросы, проверки доступа, ошибки), но +не отражает **внутреннюю бизнес-логику** приложения. + +Ручное логирование используется для фиксации действий и изменений, важных **с точки зрения +продукта** — того, что видит или делает пользователь. + +## Назначение + +Ручные логи применяются для описания событий, которые отражают состояние системы и бизнес-процессы: + +- создание, изменение и удаление сущностей; +- начало и завершение процессов; +- изменение статуса или состояния; +- ошибки бизнес-операций; +- любые другие действия, значимые для анализа и аудита. + +> Такое логирование помогает понять, **что происходило внутри системы**, даже без включённой отладки +> или доступа к базе данных. + +## Правила оформления логов + +### Общие принципы + +Сообщения должны быть: + +- **краткими** — не длиннее одной фразы; +- **человеко-читаемыми** — понятными без знания кода; +- **однозначными** — описывать факт, а не процесс; +- **на английском**, **в прошедшем времени**, **без артиклей и пунктуации**. + +Если операция завершилась неудачей, сообщение должно оканчиваться на `failed`. + +Логирование выполняется **после завершения** операции — независимо от результата. + +### Примеры сообщений + +**Успешные операции:** + +- `Project created` +- `Contest launched` +- `Review assigned` +- `Evaluation completed` + +**Ошибки и неудачи:** + +- `Project update failed` +- `File upload failed` +- `Review submission failed` + +**Неправильные формулировки:** + +- `Creating project...` — отражает процесс, а не факт. +- `Contest ${id} launched` — содержит шаблон и динамику. +- `Project created successfully!` — избыточно и разговорно. + +### Дополнительные данные + +Контекст события передаётся отдельным объектом (`details`). Включайте только данные, которые +действительно помогают при анализе. + +**Рекомендации:** + +- Всегда указывайте **идентификаторы** (`project`, `contest`, `user` и т. д.). +- В идентификаторах избегайте суфиксов `id` (пример: `project` вместо `projectId`). +- Добавляйте **причину** (`reason`) при ошибках или отказах. +- Указывайте **новое состояние** (`status`, `result`) при изменениях. +- Не включайте чувствительные данные — токены, e-mail, пароли, персональные сведения. + +**Пример:** + +```ts +logger.log('Project created', { project: 42, user: 7 }); +logger.error('Project update failed', { project: 42, reason: 'invalid_status' }); +``` + +## Бизнес-события + +### Что считается бизнес-событием + +Бизнес-события описывают **действия и изменения на уровне предметной области**, например: + +- `Contest launched` — запуск конкурса; +- `Project submitted` — заявка подана; +- `Review assigned` — эксперт назначен; +- `Evaluation completed` — оценка завершена. + +> Все бизнес-события логируются на уровне `info` (`logger.log()`), а ошибки бизнес-операций — на +> уровне `error`. + +### Длительные процессы + +Для долгих операций допускается логирование **двух фаз**: + +- начало — ` started`; +- завершение — `` (применяется обычные правила оформления сообщений). + +**Пример:** + +```ts +logger.log('File export started', { contest: 3 }); +logger.log('File exported', { contest: 3, durationMs: 14250 }); +logger.error('File export failed', { contest: 3, reason: 'timeout' }); +``` + +Такой подход позволяет отслеживать прогресс и измерять длительность операций. + +### Зачем нужны бизнес-события + +- Повышают прозрачность бизнес-процессов. +- Используются при анализе и аудите. +- Позволяют отследить жизненный цикл сущностей — от создания до завершения. +- Упрощают поиск причин ошибок на уровне сценариев пользователей. + +## Чек-лист перед ревью + +- [ ] Контекст логгера задан. +- [ ] Сообщение короткое, на английском и в прошедшем времени. +- [ ] В `details` указаны идентификаторы и причина (если применимо). +- [ ] Уровень логирования выбран корректно (`info` или `error`). +- [ ] Лог не дублирует автоматические записи (HTTP, RBAC). +- [ ] Отсутствуют конфиденциальные данные. diff --git a/docs/55-logging/05-testing.md b/docs/55-logging/05-testing.md new file mode 100644 index 0000000..60c73ae --- /dev/null +++ b/docs/55-logging/05-testing.md @@ -0,0 +1,160 @@ +# Тестирование логирования + +## Подключение + +Для тестов следует подключать специальный модуль — **`LocalLoggerModule.forTesting()`**. Он +сохраняет логи в памяти и предоставляет к ним доступ через `TestingLogger`. + +Это позволяет: + +- перехватывать все вызовы логгера внутри тестируемого кода; +- сохранять их в память вместо вывода в консоль; +- проверять, какие именно логи были собраны за время теста. + +Пример подключения через NestJS `TestingModule`: + +```ts +let logger: TestingLogger; + +beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [LocalLoggerModule.forTesting()], + }).compile(); + + logger = module.get(TestingLogger); +}); +``` + +После такого подключения любые вызовы вида: + +```ts +this.logger.log('Project created'); +this.logger.error('Project update failed'); +``` + +будут сохранены внутри `TestingLogger`, и вы сможете проверить их в тестах при помощи матчеров: + +```ts +logger.withMessage('Project created').withLevel('info').toBeLoggedOnce(); +logger.withMessage('Project update failed').withLevel('error').toBeLogged(); +``` + +## Цепочка фильтров + +Методы фильтрации (`withMessage`, `withLevel`, `withContext`, `withDetails`) можно вызывать +цепочкой, для более точного сопоставления логов: + +```ts +logger + .withMessage('Project created') + .withLevel('info') + .withContext('Projects') + .withDetails({ user: 5 }) + .toBeLoggedOnce(); +``` + +Метод `withDetails` поддерживает частичное сравнение объекта, а также может проверять поля `level`, +`context` и `message`: + +```ts +logger + .withMessage('Project created') + .withDetails({ + level: 'info', + context: 'Projects', + user: 5, + }) + .toBeLoggedOnce(); +``` + +### Просмотр найденных записей + +Если нужно отладить фильтр, и просмотреть, какие записи были найдены по текущему фильтру, можно +использовать метод `showLogs()`: + +```ts +logger.withLevel('error').showLogs().toBeLogged(); +``` + +### Методы проверки + +| Метод | Описание | +| -------------------- | --------------------------------------------------------- | +| `toBeLogged()` | Проверяет, что хотя бы одна запись соответствует фильтру. | +| `toBeNotLogged()` | Проверяет, что таких записей нет. | +| `toBeLoggedOnce()` | Проверяет, что запись встречается ровно один раз. | +| `toBeLoggedTimes(n)` | Проверяет, что запись встречается `n` раз. | + +Все методы выбрасывают ошибку, если условие не выполняется. + +> **Важно:** Методы проверки **обязательно** должны вызываться, иначе проверки так и не будет. + +### Группирование проверок + +Если в тесте нужно проверить несколько событий с общим набором полей, можно использовать такой +приём: + +```ts +const rbacLogger = logger.withContext('RBAC'); + +rbacLogger.withMessage('Access granted').toBeLoggedTimes(2); + +rbacLogger.withMessage('Access denied').toBeLoggedOnce(); +``` + +Это удобно для группировки проверок по контексту или другим общим параметрам. + +## Примеры использования + +### Проверка бизнес-события + +```ts +it('должен логировать создание проекта', () => { + service.createProject({ user: 7 }); + + logger + .withMessage('Project created') + .withLevel('info') + .withDetails({ user: 7 }) + .toBeLoggedOnce(); +}); +``` + +### Проверка ошибок + +```ts +it('должен логировать ошибку при невалидном статусе', () => { + service.updateProjectStatus(42, 'invalid'); + + logger.withMessage('Project update failed').withLevel('warn').toBeLogged(); +}); +``` + +### Проверка отсутствия логов + +```ts +it('не должен логировать ничего при успешном выполнении без событий', () => { + service.doNothing(); + + logger.any().toBeNotLogged(); +}); +``` + +### Очистка + +```ts +beforeEach(() => { + logger.clear(); +}); +``` + +Используйте `clear()` для сброса состояния между тестами, если экземпляр `TestingLogger` +переиспользуется в нескольких сценариях. + +## Рекомендации + +- `TestingLogger` предназначен **только для тестов** — в рабочем коде используется обычный + `Logger`. +- Проверяйте **факт логирования**, а не реализацию — тесты должны оставаться устойчивыми к + внутренним изменениям. +- Используйте `toBeNotLogged()` для сценариев, где событие **не должно** быть зафиксировано. diff --git a/docs/55-logging/_category_.yml b/docs/55-logging/_category_.yml new file mode 100644 index 0000000..0966109 --- /dev/null +++ b/docs/55-logging/_category_.yml @@ -0,0 +1 @@ +label: 'Логирование' diff --git a/docs/60-common-modules/_category_.yml b/docs/60-common-modules/_category_.yml new file mode 100644 index 0000000..990ddec --- /dev/null +++ b/docs/60-common-modules/_category_.yml @@ -0,0 +1 @@ +label: 'Технические модули' diff --git a/docs/60-common-modules/auth/_category_.yml b/docs/60-common-modules/auth/_category_.yml new file mode 100644 index 0000000..2437d82 --- /dev/null +++ b/docs/60-common-modules/auth/_category_.yml @@ -0,0 +1 @@ +label: 'Auth модуль и gateway' diff --git a/docs/60-common-modules/auth/intro.md b/docs/60-common-modules/auth/intro.md new file mode 100644 index 0000000..e3df3b4 --- /dev/null +++ b/docs/60-common-modules/auth/intro.md @@ -0,0 +1,157 @@ +# Принцип и сценарии + +## Глоссарий + +- **гость** не аутентифицированный посетитель +- **пользователь** аутентифицированный посетитель + +## Назначение и требования к токенам + +### Access token + +- Короткоживущий многоразовый токен +- JWT +- За счет короткого времени жизни дополнительной проверки не требуется + +### Refresh token + +- Предназначен для одноразового получения нового комплекта токенов +- Токен должен храниться в базе данных и содержать следующую информацию: + - Разрешение на генерацию токена + - предыдущий refresh token + - аутентификация пользователя + - регистрация пользователя +- Т.к. токен одноразовый и периодически обновляется, то нельзя использовать sessionStorage для его + хранения + +## Технические сценарии + +### Гость на сайте + +```mermaid +sequenceDiagram + participant C as Client + participant G as Gateway + participant S as Server + + C->>G: Request without access token + G->>S: Request without user info + S->>G: Response + G->>C: Response without tokens +``` + +### Гость логинится + +```mermaid +sequenceDiagram + participant C as Client + participant G as Gateway + participant S as Server + + C->>S: Credentials + Note over S: Check credentials + alt valid and user active + S->>C: access_token, refresh_token + else invalid or user not active + S-->>C: 422 error + end +``` + +### Гость регистрируется + +```mermaid +sequenceDiagram + participant C as Client + participant G as Gateway + participant S as Server + + C->>S: Credentials + Note over S: Check for no access token + Note over S: Check credentials + alt valid and user active + S->>C: Congratulation page with redirect on timeout to login + else invalid or user not active + S-->>C: 422 error + end +``` + +### Пользователь выполняет запрос с корректным токеном + +```mermaid +sequenceDiagram + participant C as Client + participant G as Gateway + participant S as Server + + C->>G: Request with access token + Note over G: Check access token + G->>S: Request with session data + alt session data changed + S->>G: Change sesstion data + G->>C: Response with new access token + else no sesstion changed + S->>C: Response + end +``` + +### Пользователь выполняет запрос с некорректным токеном + +```mermaid +sequenceDiagram + participant C as Client + participant G as Gateway + participant S as Server + + C->>G: Request with access and refresh tokens + Note over G: Check access token + Note over G: Access token invalid + Note over G: Check refresh token + alt refresh token valid, user is active + G->>S: Request with sesstion data + S->>G: Response with/without session data + Note over G: Create new tokens with session data + G->>C: Response with new access and refresh tokens + else refresh token expared + G-->>C: 401 Token expired + else refresh token already used + Note left of G: Leak warning + G-->>C: 419 Token already used, leak warning + end +``` + +Если `refresh token` уже был ранее использован, то это может означать что токен ранее утек, потому +пользователю надо об этом сообщить, и, возможно заблокировать все refresh token'ы, выпущенные +благодаря потенциально утекшему + +### Пользователь обновляет токены + +```mermaid +sequenceDiagram + participant C as Client + participant G as Gateway + participant S as Server + + C->>G: Request with access and refresh tokens + Note over G: Check refresh token + alt refresh token valid, user is active + Note over G: Create new tokens with session data from access token + G->>C: Response with new access and refresh tokens + else refresh token expared + G-->>C: 401 Token expired + else refresh token already used + Note left of G: Leak warning + G-->>C: 419 Token already used, leak warning + end +``` + +Если `refresh token` уже был ранее использован, то это может означать что токен ранее утек, потому +пользователю надо об этом сообщить, и, возможно заблокировать все refresh token'ы, выпущенные +благодаря потенциально утекшему + +### Пользователь логинится + +При переходе пользователя на страницу логина его перенаправляет на главную + +### Пользователь регистрируется + +Регистрация недоступна для аутентифицированного пользователя diff --git a/docs/60-common-modules/configs.md b/docs/60-common-modules/configs.md new file mode 100644 index 0000000..40d0f9b --- /dev/null +++ b/docs/60-common-modules/configs.md @@ -0,0 +1,31 @@ +# Настройки + +## Порядок применения настроек + +Настройки берутся из следующих мест (в порядке убывания приоритета): + +### Переменные окружения + +Ключ для каждой настройки формируется как SCREAMING_SNAKE_CASE из полного пути к настройке, включая +scope + +### .env файл + +Значения берутся из `.env` файла (для `NODE_ENV` = `test` из файла `.env.test`) + +### Значения по умолчанию + +Значения по умолчанию указаны в коде приложения + +## Настройки для тестирования + +Переменные окружения для теста можно указать, подменив значение провайдера `VIRTUAL_ENV`: + +```ts +const module: TestingModule = await Test.createTestingModule({ + imports: [ConfigModule], +}) + .overrideProvider(VIRTUAL_ENV) + .useValue({ NEW_OPTION: 'someValue' }) + .compile(); +``` diff --git a/docs/60-common-modules/data-store/00-overview.md b/docs/60-common-modules/data-store/00-overview.md new file mode 100644 index 0000000..70142a8 --- /dev/null +++ b/docs/60-common-modules/data-store/00-overview.md @@ -0,0 +1,61 @@ +# Общие концепции + +## Что такое DataStoreModule + +`DataStoreModule` — это сервисный модуль в NestJS, предназначенный для хранения **внутренних данных +feature-модулей**. Он работает через концепцию **bucket** (аналог внутреннего S3 или localStorage +для конкретного модуля). + +> Важно: `DataStoreModule` не имеет отношения к S3 и используется только для хранения служебных +> данных конкретного модуля. + +Каждый модуль получает собственное пространство хранения, изолированное от других. Даже если два +разных модуля используют bucket с одинаковым именем, они всё равно будут независимы. + +--- + +## Основные концепции + +### Bucket + +- Логическое пространство хранения внутри модуля. +- Для каждого конкурса используется отдельный bucket. +- Если требуется хранить данные, не связанные с конкурсом, используется `contestId = 0`. +- Имеет имя (например, `"test"`). +- Может использоваться для разных типов данных: настройки, результаты модерации и т.д. + +Пример: + +- `bucket: settings` → хранение общих настроек модуля. +- `bucket: moderationResults` → хранение результатов модерации. + +### Feature-модуль + +Каждый бизнес-модуль (например, `FooModule`, `BarModule`) подключает `DataStoreModule` через метод +`forFeature`. В результате внутри модуля можно работать с одним или несколькими bucket’ами. + +--- + +## Поддерживаемые сценарии + +- **Работа в production** — `forRoot` подключает модуль к базе данных, а `forFeature` регистрирует + bucket’ы для конкретного feature. +- **Тестирование** — `forTesting` создаёт in-memory версию, совместимую по API с production, но + без использования БД. + +--- + +## Зачем использовать DataStoreModule + +- Унифицированное API для хранения служебных данных модулей без необходимости создания собственных + таблиц. +- Автоматическое разделение данных между модулями и конкурсами. +- Простая интеграция в любой feature-модуль. + +### Когда использовать DataStoreModule + +- Временное хранение данных, не требующих сложных запросов. +- Хранение данных, не требующих частого обращения и сложных запросов. +- Хранение данных при прототипировании. + +--- diff --git a/docs/60-common-modules/data-store/01-usage.md b/docs/60-common-modules/data-store/01-usage.md new file mode 100644 index 0000000..e7dbce7 --- /dev/null +++ b/docs/60-common-modules/data-store/01-usage.md @@ -0,0 +1,98 @@ +# Использование + +## Подключение в AppModule + +В корневом модуле приложения необходимо инициализировать `DataStoreModule` с помощью метода +`forRoot`. В production используется хранилище на базе БД. + +```ts +// app.module.ts +import { Module } from '@nestjs/common'; +import { DataStoreModule } from '@src/server/data-store'; +import { FooModule } from './foo/foo.module'; + +@Module({ + imports: [DataStoreModule.forRoot(), FooModule], +}) +export class AppModule {} +``` + +--- + +## Подключение bucket в feature-модуле + +Каждый feature-модуль определяет, какие bucket’ы ему нужны, через метод forFeature. + +```ts +// foo.module.ts +import { Module } from '@nestjs/common'; +import { DataStoreModule } from '@src/server/data-store'; +import { FooService } from './foo.service'; + +@Module({ + imports: [DataStoreModule.forFeature('foo', ['settings'])], + providers: [FooService], + exports: [FooService], +}) +export class FooModule {} +``` + +В примере выше модуль `foo` получает доступ к bucket с именем `settings`. + +> Важно: В feature-модуле будут доступны только bucket'ы указанные в `forFeature`. Попытка +> инжектировать не объявленный bucket приведёт к ошибке. + +--- + +## Инжектирование bucket в сервис + +Для работы с bucket используется декоратор `@InjectDataStore(bucketName)`. Каждый сервис получает +свой экземпляр DataStore для конкретного bucket. + +```ts +// foo.service.ts +import { Injectable } from '@nestjs/common'; +import { InjectDataStore, DataStore } from '@src/server/data-store'; + +@Injectable() +export class FooService { + constructor( + @InjectDataStore('settings') + private readonly bucket: DataStore, + ) {} + + async saveValue(key: string, value: string) { + await this.bucket.set(1, key, { value }); + } + + async loadValue(key: string) { + return this.bucket.get(1, key); + } +} +``` + +--- + +## Независимость bucket’ов разных модулей и конкурсов + +Даже если несколько модулей используют bucket с одинаковым именем (`'test'`), они будут полностью +изолированы. + +Пример: + +- FooModule с bucket `test` и +- BarModule с bucket `test` + +получат разные хранилища, и данные не будут пересекаться. + +Тоже самое и для разных конкурсов: данные для `contestId = 1` и `contestId = 2` будут храниться +отдельно. + +--- + +## Резюме + +- В `AppModule` подключаем `DataStoreModule.forRoot(...)`. +- В каждом feature-модуле объявляем нужные bucket’ы через `forFeature(...)`. +- Доступ к bucket в сервисе осуществляется через `@InjectDataStore`. +- Bucket’ы одного имени в разных модулях изолированы друг от друга. diff --git a/docs/60-common-modules/data-store/02-testing.md b/docs/60-common-modules/data-store/02-testing.md new file mode 100644 index 0000000..a4561c7 --- /dev/null +++ b/docs/60-common-modules/data-store/02-testing.md @@ -0,0 +1,55 @@ +# Тестирование + +## Зачем нужен тестовый режим + +Для юнит- и интеграционных тестов используется метод `forTesting`. В этом случае `DataStoreModule` +работает на **in-memory движке**: + +- API полностью совпадает с production-режимом, +- данные не сохраняются между перезапусками, +- не требуется подключение к реальной базе данных. + +--- + +## Подключение в тестах + +Вместо `forRoot` в тестовом модуле подключается `forTesting`. + +```ts +// example.spec.ts +import { Test } from '@nestjs/testing'; +import { DataStoreModule } from '@src/server/data-store'; +import { FooModule } from './foo/foo.module'; + +describe('FooModule', () => { + it('Инициализация', async () => { + const module = await Test.createTestingModule({ + imports: [DataStoreModule.forTesting(), FooModule], + }).compile(); + + expect(() => module.createNestApplication()).not.toThrow(); + }); +}); +``` + +--- + +## Доступ к данным + +Для задания данных в тестах достаточно получить бакет через сервис модуля: + +```ts +it('Запись данных в bucket', async () => { + const module = await Test.createTestingModule({ + imports: [DataStoreModule.forTesting(), FooModule, BarModule], + }).compile(); + + const dataStore = module.get(DataStoreService); + const barService = module.get(BarService); + + const bucket = dataStore.bucket('bar', 'test'); // доступ к bucket "test" модуля "bar" + await bucket.set(1, 'key', { value: 'bar' }); + + expect(await barService.bucket.get(1, 'key')).toEqual({ value: 'bar' }); +}); +``` diff --git a/docs/60-common-modules/data-store/_category_.yml b/docs/60-common-modules/data-store/_category_.yml new file mode 100644 index 0000000..d414568 --- /dev/null +++ b/docs/60-common-modules/data-store/_category_.yml @@ -0,0 +1 @@ +label: 'DataStore' diff --git a/docs/60-common-modules/docs.md b/docs/60-common-modules/docs.md new file mode 100644 index 0000000..96e91a7 --- /dev/null +++ b/docs/60-common-modules/docs.md @@ -0,0 +1,56 @@ +# Документы и изображения + +## Общее устройство + +Все загружаемые файлы хранятся в S3 хранилище, на сервер приложения файлы могут сохраняться только +на время обработки и только в директорию для временных файлов. + +Изображение так же является документом, только с дополнительными возможностями + +Индентификатор документа - это непорядковый длинный код + +Отдельной системы контроля доступа нет, если пользователь знает ID файла - он может к нему +обратиться + +Обновление файлов не предусмотрено, если нужно обновить - то загружается новый файл, и в нужном +месте указывается уже новый идентификатор, обновлять можно только название и описание. + +## Документ + +### Сохраняемая информация + +В базе данных о каждом документе хранится информация. + +- оригинальное имя файла (используется при скачивании) +- время загрузки файла +- пользователь, загрузивший файл +- mime тип файл +- общий тип файла, пока только документ или изображение +- так же файлу можно задать название документа и описание +- размер файла + +### Доступные действия + +- получение информации о файле (`GET /files/:id`) +- получение информации о нескольких файлах (`GET /files/many/:id,:id,:id`) +- просмотреть (`GET /files/:id/view`) +- скачать (`GET /files/:id/download`) отличается от **посмотреть** тем, что указываются заголовки + для скачивания +- загрузка файла (`POST /files`), возвращает структуру `IDocument` или дочернюю + +## Изображение + +Изображение так же являются документом, и для него доступны те же действия и возможности + +### Сохраняемая информация + +Дополнительно сохранятся размер изображения + +### Дополнительные доступные действия + +- просмотреть с определенными размерами (`GET /files/:id/view/:size`) + + где `size` это Enum с заранее определенными размерами и параметрами + +Для просмотра изображения используется сервер, на лету меняющий размеры изображения, для этого +сервер приложения выполняет редирект на специально подготовленный адрес diff --git a/docs/70-common-ui/_category_.yml b/docs/70-common-ui/_category_.yml new file mode 100644 index 0000000..e93b7b1 --- /dev/null +++ b/docs/70-common-ui/_category_.yml @@ -0,0 +1 @@ +label: 'UI/UX' diff --git a/docs/70-common-ui/tabs.md b/docs/70-common-ui/tabs.md new file mode 100644 index 0000000..e57f268 --- /dev/null +++ b/docs/70-common-ui/tabs.md @@ -0,0 +1,84 @@ +# Вкладки + +Допустим нам нужно разделить страницу `users/page.tsx` на 2 вкладки: + +- Основные данные +- Организации + +При открытии страницы по умолчанию должна открываться вкладка "Основные данные". + +Организуем структуру следующим образом: + +```tsx +users / page.tsx; // корневая страница +general / page.tsx; // страница вкладки +organizators / page.tsx; // страница вкладки +``` + +## Определение вкладок + +Добавим файл с определением вкладок: + +```ts +// users/tabs.ts + +import { ITabs } from '@src/client/uikit/TabBar'; + +export enum UserTab { + General = 'general', + Organizators = 'organizators', +} + +export const clientUserTabs: ITabs = [ + { + label: 'Основные данные', + slug: UserTab.General, + }, + { + label: 'Организации', + slug: UserTab.Organizators, + }, +]; +``` + +И сделать по странице для каждой вкладки, _имя страницы_ должно совпадать с _текстовым значением_ +варианта enum. + +## Страницы вкладок + +```tsx +// users/general/page.tsx + +import { clientUserTabs } from '../tabs'; + +export default async function UserViewPage({ params }: { id: string }) { + return ( + + + + + + ); +} +``` + +Компонент `TabBar` предназначен для использования непосредственно внутри `Card`. Если поместить его +в `Card.Header`, стили могут сломаться. + +## Корневая страница + +Корневые страницы (`users/page.tsx` в нашем примере) не поддерживаются, поскольку имя страницы +привязано к варианту enum, а корневая страница была бы пустой строкой. + +Если она вам нужна, создайте страницу для редиректа: + +```tsx +// users/page.tsx +import { redirect } from 'next/navigation'; +interface IProps { + params: { id: string }; +} +export default function UserViewPage({ params }: IProps) { + redirect(`/admin/users/${params.id}/general`); +} +``` diff --git a/docs/90-deploy/01-ci-cd.md b/docs/90-deploy/01-ci-cd.md new file mode 100644 index 0000000..88516ae --- /dev/null +++ b/docs/90-deploy/01-ci-cd.md @@ -0,0 +1,64 @@ +# CI/CD и деплой + +Система CI построена на **Gitea Actions** (`.gitea/workflows/`). Два основных сценария: ветка и тег. + +## Ветки → стенды + +При каждом `git push` в любую ветку (включая `master`) автоматически: + +1. Собирается проект (`yarn build`) +2. Публикуются Docker-образы в registry `git.jt4d.ru/1vit/more/`: + - `web:` + - `api:` + - `ingress:` +3. Разворачивается стенд на стейджинге по адресу `http://.` + +Каждый стенд получает изолированную базу данных. Миграции запускаются автоматически при деплое. + +При удалении ветки стенд и образы удаляются автоматически. + +## Теги → релизные образы + +При `git push` тега (например `v1.18.0`) запускается workflow `release.yaml`: + +1. Собирается проект +2. Публикуются Docker-образы с **двумя тегами** — именем тега и `latest`: + - `git.jt4d.ru/1vit/more/web:v1.18.0` + - `git.jt4d.ru/1vit/more/web:latest` + - Аналогично для `api` и `ingress` + +Стенд при push тега **не создаётся**. + +Чтобы выпустить релиз: + +```bash +git tag v1.18.0 +git push origin v1.18.0 +``` + +## Продакшн-деплой + +Для деплоя на продакшн используется `docker-compose.production.yml`. Образы берутся из Gitea +registry. + +Развернуть последнюю версию (`latest`): + +```bash +PROD_HOST=example.com \ +DB_HOST=db DB_USERNAME=user DB_PASSWORD=pass DB_DATABASE=mydb \ +docker compose -f docker-compose.production.yml up -d +``` + +Развернуть конкретную версию: + +```bash +TAG=v1.18.0 PROD_HOST=example.com \ +DB_HOST=db DB_USERNAME=user DB_PASSWORD=pass DB_DATABASE=mydb \ +docker compose -f docker-compose.production.yml up -d +``` + +После деплоя не забыть прогнать миграции: + +```bash +docker exec 1vit_more_prod_api node_modules/.bin/typeorm migration:run -d data-source.js +``` diff --git a/docs/90-deploy/_category_.yml b/docs/90-deploy/_category_.yml new file mode 100644 index 0000000..5a537f8 --- /dev/null +++ b/docs/90-deploy/_category_.yml @@ -0,0 +1 @@ +label: 'Поставка'