refactor: align frontend with FSD architecture
Some checks failed
CI/CD Pipeline / Quality Gate (push) Failing after 2m37s
CI/CD Pipeline / Build and Deploy Dev via Compose (push) Has been skipped

This commit is contained in:
Gleb Korotkiy
2026-06-22 12:33:23 +03:00
parent 3adad7dbbf
commit 67ec34d619
95 changed files with 7802 additions and 5053 deletions

123
docs/architecture.md Normal file
View File

@@ -0,0 +1,123 @@
# Архитектура проекта
Дата актуализации: 2026-06-22
## Базовый принцип
Проект следует Feature-Sliced Design в прагматичном варианте: слой добавляется только тогда,
когда у него есть реальная ответственность. Главный рабочий поток сейчас построен вокруг
страниц `auth` и `dashboard`, защищенного layout и общего shared-инструментария.
## Слои
```text
src/
app/ # инициализация приложения, router, providers, базовые стили
pages/ # route-level страницы и их page-specific model/api/ui
widgets/ # крупные композиционные блоки, собирающие entities/features/shared
entities/ # доменные сущности
shared/ # инфраструктура, API client, базовый UI, helpers, local UI-kit
```
`features` сейчас не используется намеренно. Например, logout используется только внутри
`widgets/protected-app-layout`, поэтому orchestration живет в widget model, а не в отдельном
feature slice. Если действие начнет использоваться в нескольких независимых flows, его можно
поднять в `features/logout-session`.
## Правила импортов
- Верхние слои могут импортировать нижние: `app -> pages -> widgets -> features -> entities -> shared`.
- Нижние слои не импортируют верхние.
- Между слайсами одного слоя не должно быть cross-imports.
- Внутри одного слайса допускаются относительные импорты между сегментами.
- Внешний код импортирует слайс через public API `index.ts`.
- Generated API не импортируется напрямую из app/pages/widgets/entities. Используется facade `@/shared/api`.
## Текущие ключевые слайсы
### `pages/dashboard`
```text
src/pages/dashboard/
api/
competenceMapAnalyticsApi.ts
model/
chartData.ts
constants.ts
dashboardStore.ts
emptyState.ts
error.ts
filterDefinitions.ts
filterDraft.ts
filters.ts
formatters.ts
queryKeys.ts
sorting.ts
tooltip.ts
types.ts
ui/
DashboardPage.vue
ReportsView.vue
CompetenceMapFilters.vue
CompetenceMapToolbar.vue
components/
styles/
```
Dashboard page владеет аналитическими фильтрами, вкладками, API wrappers, DTO-to-view helpers,
табличной сортировкой, tooltip templates и page-specific компонентами. TanStack Query отвечает
за server state cache; ручной response cache удален.
### `pages/auth`
```text
src/pages/auth/
model/
loginSchema.ts
redirect.ts
useAuthLogin.ts
ui/
```
UI формы не содержит schema и redirect validation. Эти правила находятся в model-сегменте и
покрыты unit-тестами.
### `widgets/protected-app-layout`
Widget собирает защищенную часть приложения: header, sidebar, page content, footer и scroll-up.
Здесь живут current-user composition, logout orchestration, sidebar route matching, route preload
и persisted expanded groups. `shared/ui/AppSidebar` остается pure UI и не знает про Vue Router.
### `shared/api`
Содержит API client, auth token primitives, refresh/session infrastructure, error normalization,
generated API и public facade. Прикладной код импортирует endpoint functions и DTO-типы через
`@/shared/api`, а не из `generated-api`.
## Generated и vendor-зоны
Эти зоны исключены из FSD/formatting-оценки как generated/vendor:
- `src/shared/api/generated-api`
- `src/shared/model/generated-zod`
- `src/shared/ui-kit`
Ручные wrappers, adapters и page models не должны попадать в эти исключения.
## Проверки архитектуры
Основная команда:
```bash
bun run check:full
```
Она запускает ESLint, Steiger, unit tests, typecheck/build и Playwright E2E. Для быстрой проверки
FSD-границ:
```bash
bun run lint:fsd
```
Перед передачей изменений заказчику `lint:fsd` должен проходить без ошибок или иметь явно
задокументированное точечное исключение.

82
docs/generated-api.md Normal file
View File

@@ -0,0 +1,82 @@
# Generated API
Дата актуализации: 2026-06-22
## Назначение
Проект использует Orval для генерации TypeScript API-клиентов и Zod-моделей из `openapi.json`.
Generated-код не редактируется вручную.
## Команда генерации
```bash
bun run apigen
```
Orval config находится в `orval.config.ts`.
## Входной файл
```text
openapi.json
```
В config есть нормализация русскоязычных тегов OpenAPI в стабильные англоязычные имена файлов:
- `Аутентификация` -> `authentication`
- `Карта компетенций: аналитика` -> `competence-map-analytics`
- `Мониторинг` -> `monitoring`
- `Пользователь` -> `user`
- `Управление пользователями` -> `user-management`
Config также умеет читать первый валидный JSON-документ, если исходный OpenAPI-файл содержит
лишние данные после первого JSON.
## Выходные директории
```text
src/shared/api/generated-api/
src/shared/model/generated-zod/
```
`generated-api` использует custom mutator:
```text
src/shared/api/axiosInstance.ts
```
Это сохраняет единое поведение API-клиента: base URL, credentials, refresh flow, error handling
и abort signal.
## Public facade
Прикладной код не должен импортировать generated-файлы напрямую. Правильный путь:
```ts
import { getCompetenceMapAnalytics, type Login } from '@/shared/api'
```
Неправильный путь:
```ts
import { api_v1_competence_map_analytics_list } from '@/shared/api/generated-api/competence-map-analytics'
```
Facade `src/shared/api/index.ts` сейчас экспортирует только используемые приложением endpoint
functions, token primitives, error helpers и DTO-типы.
## Правила изменений
- Не редактировать файлы в `generated-api` и `generated-zod` вручную.
- При изменении backend контракта обновить `openapi.json` и выполнить `bun run apigen`.
- После генерации выполнить `bun run check:full`.
- Если новому экрану нужен endpoint, сначала добавить осознанный export в `src/shared/api/index.ts`
или отдельный wrapper, а не импортировать generated-файл напрямую.
- Если DTO требует page-specific адаптации, mapper должен жить рядом с owner-страницей или рядом
с API wrapper, а не в generated-зоне.
## Tooling exclusions
Generated-директории исключены из Steiger и Prettier через `steiger.config.ts` и `.prettierignore`.
Это не разрешение писать ручной код внутри generated-зон. Исключение нужно только для стабильной
работы tooling после Orval generation.

View File

@@ -576,12 +576,12 @@ Query params:
Страница имеет четыре серверных состояния. Они отличаются составом данных, но используют один и тот же
endpoint `/api/v1/competence-map/analytics/`.
| `association_type` | KPI | `main_block.type` | `region_ranking` | `top_industries.mode` | `trend_block` | `employee_stats` |
|---|---|---|---|---|---|---|
| `all` | 4 карточки по всем объединениям | `all_associations_composition` | 3 метрики по типам объединений | `stacked_by_association_type` | `opk_participation_by_year` | `null` |
| `sez` | 4 карточки по ОЭЗ | `sez_types_and_priority_directions` | ОЭЗ + организации ОПК | `single_association_type` | `null` | сотрудники ОПК-резидентов ОЭЗ и сотрудники ОЭЗ |
| `technoparks` | 4 карточки по технопаркам | `technopark_area_distribution` | технопарки + организации ОПК | `single_association_type` | `null` | сотрудники ОПК-резидентов технопарков и их доля |
| `clusters` | 4 карточки по кластерам | `cluster_specializations` | кластеры + организации ОПК | `single_association_type` | `cluster_creation_dynamics` | `null` |
| `association_type` | KPI | `main_block.type` | `region_ranking` | `top_industries.mode` | `trend_block` | `employee_stats` |
| ------------------ | ------------------------------- | ----------------------------------- | ------------------------------ | ----------------------------- | --------------------------- | ----------------------------------------------- |
| `all` | 4 карточки по всем объединениям | `all_associations_composition` | 3 метрики по типам объединений | `stacked_by_association_type` | `opk_participation_by_year` | `null` |
| `sez` | 4 карточки по ОЭЗ | `sez_types_and_priority_directions` | ОЭЗ + организации ОПК | `single_association_type` | `null` | сотрудники ОПК-резидентов ОЭЗ и сотрудники ОЭЗ |
| `technoparks` | 4 карточки по технопаркам | `technopark_area_distribution` | технопарки + организации ОПК | `single_association_type` | `null` | сотрудники ОПК-резидентов технопарков и их доля |
| `clusters` | 4 карточки по кластерам | `cluster_specializations` | кластеры + организации ОПК | `single_association_type` | `cluster_creation_dynamics` | `null` |
Требования:
@@ -2287,16 +2287,16 @@ Backend должен гарантировать:
### Status code matrix
| Status | Когда возвращается |
|---|---|
| `200 OK` | Успешный ответ, включая валидные фильтры без данных. |
| `304 Not Modified` | Conditional GET по `If-None-Match` или `If-Modified-Since`, если представление не изменилось. |
| `400 Bad Request` | Некорректный query param, неизвестный query param, повтор параметра, неверный формат значения. |
| `401 Unauthorized` | Пользователь не аутентифицирован. |
| `403 Forbidden` | Пользователь аутентифицирован, но не имеет доступа к странице или разделу. |
| `429 Too Many Requests` | Превышен rate limit. |
| `500 Internal Server Error` | Непредвиденная ошибка backend-а. |
| `503 Service Unavailable` | Временная недоступность сервиса или источника аналитических данных. |
| Status | Когда возвращается |
| --------------------------- | ---------------------------------------------------------------------------------------------- |
| `200 OK` | Успешный ответ, включая валидные фильтры без данных. |
| `304 Not Modified` | Conditional GET по `If-None-Match` или `If-Modified-Since`, если представление не изменилось. |
| `400 Bad Request` | Некорректный query param, неизвестный query param, повтор параметра, неверный формат значения. |
| `401 Unauthorized` | Пользователь не аутентифицирован. |
| `403 Forbidden` | Пользователь аутентифицирован, но не имеет доступа к странице или разделу. |
| `429 Too Many Requests` | Превышен rate limit. |
| `500 Internal Server Error` | Непредвиденная ошибка backend-а. |
| `503 Service Unavailable` | Временная недоступность сервиса или источника аналитических данных. |
Для collection/report endpoint-ов не использовать `404 Not Found`, если path существует, но по валидным фильтрам
нет данных. В этом случае нужен `200 OK` с пустыми массивами.

View File

@@ -320,10 +320,10 @@ GET /api/v1/competence-map/analytics/?association_type=technoparks
Обязательные метрики для ОЭЗ:
| `code` | Назначение | `value_type` |
|---|---|---|
| `opk_resident_employees_count` | численность сотрудников организаций ОПК-резидентов ОЭЗ | `integer` |
| `sez_employees_count` | общая численность сотрудников ОЭЗ | `integer` |
| `code` | Назначение | `value_type` |
| ------------------------------ | ------------------------------------------------------ | ------------ |
| `opk_resident_employees_count` | численность сотрудников организаций ОПК-резидентов ОЭЗ | `integer` |
| `sez_employees_count` | общая численность сотрудников ОЭЗ | `integer` |
### Технопарки. Требуемые поля
@@ -357,10 +357,10 @@ GET /api/v1/competence-map/analytics/?association_type=technoparks
Обязательные метрики для технопарков:
| `code` | Назначение | `value_type` |
|---|---|---|
| `opk_resident_employees_count` | численность сотрудников организаций ОПК-резидентов технопарков | `integer` |
| `opk_resident_employees_share_percent` | доля сотрудников организаций ОПК-резидентов технопарков | `percent` |
| `code` | Назначение | `value_type` |
| -------------------------------------- | -------------------------------------------------------------- | ------------ |
| `opk_resident_employees_count` | численность сотрудников организаций ОПК-резидентов технопарков | `integer` |
| `opk_resident_employees_share_percent` | доля сотрудников организаций ОПК-резидентов технопарков | `percent` |
### Требования
@@ -421,13 +421,13 @@ GET /api/v1/competence-map/analytics/?association_type=technoparks
## Сводка исправлений по backend response
| Проблема | Endpoint | Поля | Что нужно сделать |
|---|---|---|---|
| Placeholder субъектов РФ | `/analytics/` для всех `association_type` | `region_ranking.rows[].region_name` | возвращать реальные названия субъектов РФ |
| Placeholder отраслей | `/analytics/` для всех `association_type` | `top_industries.rows[].industry_name` | возвращать реальные названия отраслей |
| Пустая таблица направлений ОЭЗ | `/analytics/?association_type=sez` | `main_block.priority_directions.rows[]` | вернуть строки направлений ОЭЗ |
| Нет карточек сотрудников ОЭЗ | `/analytics/?association_type=sez` | `employee_stats.items[]` | вернуть две KPI-метрики сотрудников |
| Нет карточек сотрудников технопарков | `/analytics/?association_type=technoparks` | `employee_stats.items[]` | вернуть две KPI-метрики сотрудников |
| Проблема | Endpoint | Поля | Что нужно сделать |
| ------------------------------------ | ------------------------------------------ | --------------------------------------- | ----------------------------------------- |
| Placeholder субъектов РФ | `/analytics/` для всех `association_type` | `region_ranking.rows[].region_name` | возвращать реальные названия субъектов РФ |
| Placeholder отраслей | `/analytics/` для всех `association_type` | `top_industries.rows[].industry_name` | возвращать реальные названия отраслей |
| Пустая таблица направлений ОЭЗ | `/analytics/?association_type=sez` | `main_block.priority_directions.rows[]` | вернуть строки направлений ОЭЗ |
| Нет карточек сотрудников ОЭЗ | `/analytics/?association_type=sez` | `employee_stats.items[]` | вернуть две KPI-метрики сотрудников |
| Нет карточек сотрудников технопарков | `/analytics/?association_type=technoparks` | `employee_stats.items[]` | вернуть две KPI-метрики сотрудников |
---

1278
docs/review.md Normal file

File diff suppressed because it is too large Load Diff

99
docs/testing.md Normal file
View File

@@ -0,0 +1,99 @@
# Testing Policy
Дата актуализации: 2026-06-22
## Обязательные проверки перед сдачей
```bash
bun run format:check
bun run check:full
```
`check:full` запускает:
- ESLint;
- Steiger FSD lint;
- Vitest unit tests;
- TypeScript typecheck;
- production build;
- Playwright E2E.
## Текущее покрытие
Unit:
- dashboard filter normalization;
- dashboard API params;
- structured query keys;
- dashboard empty state;
- chart adapters;
- sorting;
- formatting/pluralization;
- tooltip escaping;
- dashboard filter draft mapping;
- dashboard filter definitions;
- `CompetenceMapFilterField` render/v-model contract;
- auth login schema;
- auth safe redirect helper;
- UI-kit entrypoint smoke.
E2E:
- redirect protected route `/dashboard` to `/auth` without auth session;
- auth form empty validation.
## Что нужно покрыть следующим этапом
Component tests:
- `CompetenceMapFilters.vue`: open/sync draft, apply, reset, cancel, disabled/loading state;
- `DashboardTabs.vue`: tab selection and region rating branch;
- `RegionRankingCard.vue`: sort controls;
- `TopIndustriesCard.vue`: empty and filled states;
- `ChartTooltipPortal.vue`: visibility and positioning contract.
E2E with mocked backend:
- authorized dashboard load;
- all/sez/technoparks/clusters tabs;
- filters dialog apply/reset/cancel;
- ranking table sort;
- region rating tab;
- mobile layout smoke.
Visual regression:
- desktop auth page;
- desktop dashboard all/sez/technoparks/clusters;
- filters dialog;
- mobile dashboard top section.
## Правила написания тестов
- Pure helpers тестировать unit-тестами без Vue mount.
- Page-specific UI тестировать рядом с owner slice.
- UI-kit internals не тестировать в page tests; для сложных компонентов использовать stubs или E2E.
- API-зависимые E2E должны использовать route mocks, а не реальный backend.
- Для security-sensitive строковых HTML templates обязательно оставлять tests на escaping.
- После изменения FSD-границ всегда запускать `bun run lint:fsd`.
## Известные build warnings
Сборка проходит, но Vite/Rolldown предупреждает:
- `INVALID_ANNOTATION` в `node_modules/@vueuse/core/dist/index.js`;
- главный chunk больше 500 kB.
Это не runtime failures. Bundle size требует отдельного performance-pass с анализатором чанков.
## Известные test warnings
Vitest component test для Vue SFC может печатать jsdom warning:
```text
Could not parse CSS stylesheet
```
Тесты при этом проходят. Это связано с парсингом runtime-injected SFC/UI-kit CSS в jsdom, а не с
ошибкой production CSS. Если warning начнет мешать CI logs, стоит добавить test setup с точечной
фильтрацией этого сообщения или вынести component tests на browser-based runner.