Files
anal-front/docs/review.md
Gleb Korotkiy 67ec34d619
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
refactor: align frontend with FSD architecture
2026-06-22 12:33:33 +03:00

1279 lines
66 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ревью проекта и план FSD/Vue рефакторинга
Дата: 2026-06-22
Проект: `analytical-panel`
Цель документа: зафиксировать техническое ревью, источники best practices, проблемы текущей реализации и пошаговый план рефакторинга без потери визуального или логического поведения приложения.
## 1. Главный принцип рефакторинга
Рефакторинг должен идти маленькими проверяемыми шагами. На каждом шаге нужно сохранять:
- тот же пользовательский сценарий авторизации;
- тот же redirect guard для защищенных страниц;
- те же запросы к backend и параметры фильтров;
- те же состояния загрузки, ошибки и пустых данных;
- те же вкладки аналитики: `all`, `sez`, `technoparks`, `clusters`, режим рейтинга регионов;
- ту же сортировку таблиц;
- те же тултипы графиков и интерактивные элементы;
- ту же адаптивную верстку;
- те же публичные UI-kit импорты, пока не будет отдельного шага по оптимизации bundle.
Визуальный редизайн не входит в первый этап. Сначала нужно привести код к поддерживаемой архитектуре, не меняя внешний вид.
## 2. Исследованные источники и применимые выводы
### 2.1. Feature-Sliced Design
Официальные источники:
- FSD Overview: https://fsd.how/docs/get-started/overview/
- FSD Layers: https://feature-sliced.design/docs/reference/layers
- FSD Slices and segments: https://feature-sliced.design/docs/reference/slices-segments
- FSD Public API: https://feature-sliced.design/docs/reference/public-api
- FSD Migration from custom architecture: https://feature-sliced.design/docs/guides/migration/from-custom
- FSD Migration from v2.0 to v2.1: https://feature-sliced.design/docs/guides/migration/from-v2-0
- FSD Handling API Requests: https://feature-sliced.design/docs/guides/examples/api-requests
- FSD Types: https://feature-sliced.design/docs/guides/examples/types
- FSD Authentication: https://feature-sliced.design/docs/guides/examples/auth
- FSD Page layouts: https://feature-sliced.design/docs/guides/examples/page-layout
- FSD Desegmentation smell: https://feature-sliced.design/docs/guides/issues/desegmented
- FSD Cross-import smell: https://feature-sliced.design/docs/guides/issues/cross-imports
- FSD Routing smell: https://feature-sliced.design/docs/guides/issues/routes
Ключевые выводы для проекта:
- FSD не требует использовать все слои. Минимально нормальная структура может состоять из `app`, `pages`, `shared`; `widgets`, `features`, `entities` добавляются только когда дают реальную пользу.
- Актуальная ментальная модель FSD v2.1: `pages first`. Сначала страница становится понятным слайсом, затем действительно переиспользуемая логика выносится ниже.
- Основное правило импортов: код внутри слайса может импортировать только слайсы слоев ниже. Исключения: `app` и `shared`, потому что в них нет бизнес-слайсов.
- Каждый слайс и каждый сегмент без слайсов должен иметь public API. Внешний код не должен импортировать внутренние файлы напрямую.
- Внутри одного слайса лучше использовать относительные импорты напрямую к нужному файлу, а не импортировать из собственного `index.ts`, чтобы не создавать цикл через barrel-файл.
- `shared/ui` и `shared/lib` не должны превращаться в одну большую бочку экспортов, если это ухудшает tree-shaking и размер bundle. Для крупных UI-наборов допустимы отдельные entrypoints.
- DTO и generated API допустимо держать в `shared/api`, особенно если они связаны между доменами. Мапперы стоит держать рядом с DTO/API-оберткой или рядом со слайсом, если маппер специфичен для страницы.
- Логика auth token storage и refresh может жить в `shared/api`, если она является частью общего API-клиента. UI-действия вроде logout лучше не прятать в `shared/ui`.
- Layout с бизнес-виджетами можно держать в `app/layouts` либо строить через slots/render props, чтобы не нарушать направление зависимостей.
- Cross-import между слайсами одного слоя является code smell и должен быть либо устранен, либо явно документирован.
- Hardcoded routing/URLs на нижних слоях размазывает ответственность. Роутинг должен концентрироваться в `app`/`pages`, а нижним слоям лучше передавать callbacks, props или route descriptors.
### 2.2. Vue 3
Официальные источники:
- Vue Style Guide, Essential: https://vuejs.org/style-guide/rules-essential
- Vue Style Guide, Strongly Recommended: https://vuejs.org/style-guide/rules-strongly-recommended.html
- Vue `<script setup>`: https://vuejs.org/api/sfc-script-setup
- Vue TypeScript with Composition API: https://vuejs.org/guide/typescript/composition-api
- Vue Composables: https://vuejs.org/guide/reusability/composables
- Vue Performance: https://vuejs.org/guide/best-practices/performance
- Vue Production Deployment: https://vuejs.org/guide/best-practices/production-deployment
- Vue Security: https://vuejs.org/guide/best-practices/security
- Vue Tooling: https://vuejs.org/guide/scaling-up/tooling
- Vue Router Lazy Loading: https://router.vuejs.org/guide/advanced/lazy-loading.html
Ключевые выводы для проекта:
- `<script setup lang="ts">` является рекомендуемым синтаксисом для Composition API в SFC.
- Для props и emits желательно использовать типизированные объявления. В TypeScript-проектах предпочтительны `defineProps<Props>()`, `defineEmits<...>()`, `defineModel<...>()` по ситуации.
- Компоненты должны быть multi-word, за исключением корневого `App`. Отключение `vue/multi-word-component-names` допустимо только временно и с понятной причиной.
- `v-for` должен иметь стабильный `key`; `v-if` и `v-for` на одном элементе нужно избегать.
- Стили компонентов приложения должны быть scoped или изолированы соглашением имен. Глобальные стили допустимы для root/layout/reset/design tokens, но не для всей страницы на 1900 строк.
- Composables нужны для переиспользуемой чистой логики; если переиспользуется и логика, и layout, это уже компонент.
- Composables должны возвращать refs/plain object так, чтобы потребитель не терял реактивность при destructuring.
- Для производительности нужно измерять bundle size, избегать лишних абстракций, применять route-level code splitting и не тянуть тяжелые части UI-kit в главный chunk без необходимости.
- Vue Router рекомендует dynamic imports для route components; проект это уже делает.
- ESLint и Prettier должны быть частью проверок перед поставкой; желательно иметь единый `check`/CI script.
- В security-контексте нельзя строить templates из недоверенного контента. В текущем коде есть HTML tooltip templates, поэтому нужно оставить явный escaping и покрыть это тестами.
### 2.3. Pinia, TanStack Query, Orval
Официальные источники:
- Pinia Defining a Store: https://pinia.vuejs.org/core-concepts/
- TanStack Query Vue Query Keys: https://tanstack.com/query/v5/docs/framework/vue/guides/query-keys
- TanStack Query Vue Dependent Queries: https://tanstack.com/query/v5/docs/framework/vue/guides/dependent-queries
- Orval Vue Query: https://orval.dev/docs/guides/vue-query/
- Orval Custom HTTP Client: https://orval.dev/docs/guides/custom-client/
Ключевые выводы для проекта:
- Pinia setup stores могут использовать composables и watchers, но store лучше держать сфокусированным и в отдельном файле.
- TanStack Query уже решает server state: кэш, stale time, retry, query keys. Ручной кэш поверх Vue Query должен быть оправдан поведением UI, иначе он усложняет модель.
- Query keys должны включать все переменные, от которых зависит query function. Строковый ключ через `join('|')` работает, но структурированный ключ с объектом фильтров проще проверять и масштабировать.
- Orval может генерировать Vue Query composables и type-safe helpers. В проекте Orval уже настроен на `vue-query` и custom mutator, но потребители обходят public API generated-модулей.
## 3. Текущее состояние проекта
### 3.1. Стек
Из `package.json`:
- Vue `^3.5.34`;
- Vite `^8.0.12`;
- TypeScript `~6.0.2`;
- Vue Router `^5.1.0`;
- Pinia `^3.0.4`;
- TanStack Vue Query `^5.101.0`;
- TanStack Vue Table `^8.21.3`;
- Orval `^8.18.0`;
- Zod `3.25.76`;
- Vee Validate `^4.15.1`;
- Tailwind CSS `^4.1.18`;
- shadcn/reka-based local UI-kit;
- Vitest and Playwright.
### 3.2. Baseline проверок
На момент ревью:
- `bun run typecheck` проходит;
- `bun run lint` проходит;
- `bun run test:unit` проходит: 4 test files, 18 tests;
- `bun run build` проходит.
Предупреждения build:
- основной `dist/assets/index-*.js` после минификации около `725.42 kB`, gzip около `234.10 kB`;
- Vite/Rolldown выводит предупреждения `INVALID_ANNOTATION` по `#__PURE__` в `node_modules/@vueuse/core/dist/index.js`;
- Vite предупреждает о chunk больше `500 kB`.
### 3.3. Структура, важная для FSD
Сейчас есть:
- `src/app/layouts`, `src/app/providers`;
- `src/app/router`;
- `src/app/styles`;
- `src/pages/auth`, `src/pages/dashboard`;
- `src/widgets/protected-app-layout`;
- `src/entities/user`;
- `src/shared/api`, `src/shared/lib`, `src/shared/model`, `src/shared/ui`, `src/shared/ui-kit`;
- root entrypoints вне слоев: `src/main.ts`, `src/App.vue`, `src/style.css`.
Крупные файлы:
- `src/pages/dashboard/ui/DashboardPage.vue`: около 487 строк;
- `src/pages/dashboard/model/dashboardStore.ts`: около 205 строк;
- `src/pages/dashboard/ui/CompetenceMapFilters.vue`: около 284 строк;
- `src/pages/dashboard/ui/components/CompetenceMapFilterField.vue`: около 98 строк;
- `src/pages/dashboard/ui/components/TrendCard.vue`: около 261 строк;
- `src/pages/dashboard/ui/components/TopIndustriesCard.vue`: около 197 строк;
- `src/style.css`: 2 строки, только CSS entrypoint;
- `src/pages/dashboard/ui/styles/sections/layout.css`: около 495 строк;
- `src/pages/dashboard/ui/styles/sections/tables.css`: около 415 строк;
- `src/pages/dashboard/ui/styles/sections/composition.css`: около 367 строк;
- `src/pages/dashboard/ui/styles/sections/charts-and-industries.css`: около 324 строк;
- `src/pages/dashboard/ui/ReportsView.vue`: около 243 строк.
Практические пороги размера файлов для этого проекта:
- page component: целевой диапазон 150-250 строк, после 300 строк нужен план декомпозиции, после 500 строк это архитектурный smell;
- widget component: целевой диапазон 150-300 строк, после 350 строк нужно проверить, не смешаны ли UI, model и адаптеры данных;
- feature/entity component: целевой диапазон 100-250 строк, после 300 строк нужно выносить подкомпоненты или composable;
- shared/ui component: целевой диапазон 80-180 строк, после 250 строк компонент обычно становится слишком специализированным для `shared`;
- composable: целевой диапазон 80-180 строк, после 220 строк нужно разделять orchestration, derived state и side effects;
- store/model file: целевой диапазон 150-300 строк, после 350 строк нужно отделять query keys, API adapters, pure mappers и UI state;
- pure helper/mapper: целевой диапазон 50-150 строк, после 200 строк нужно проверить cohesion и тестируемые единицы;
- CSS-файл компонента или feature: целевой диапазон 150-300 строк, после 400 строк нужно дробить по зонам ответственности;
- глобальный CSS entrypoint: только reset, tokens, base layers и импорты; страничные стили не должны жить в root `style.css`;
- generated, vendor, icon packs и локальный `shared/ui-kit`, если он осознанно хранится как design system/vendor layer, не оцениваются по этим порогам.
Это не жесткий лимит ради числа строк. Файл может быть длиннее, если он механически сгенерирован или содержит однотипную таблицу конфигурации. Для ручного прикладного кода превышение порога означает обязательное ревью ответственности файла.
### 3.4. Диагностика Steiger
Команда первичного аудита: `bunx steiger src`
Результат: 12 errors, 1 warning, без автофиксов.
Для дальнейшей работы добавлена воспроизводимая команда `bun run lint:fsd` и конфиг `steiger.config.ts`. В конфиге осознанно исключены generated/vendor-зоны:
- `src/shared/api/generated-api/**`;
- `src/shared/model/generated-zod/**`;
- `src/shared/ui-kit/**`.
Эти исключения не скрывают архитектурные проблемы прикладного кода. Они отделяют ручной FSD-код от generated API, generated zod-моделей и локального UI-kit, который сейчас фактически живет как отдельный design system.
Текущий результат `bun run lint:fsd`: проходит без ошибок.
Точечное архитектурное исключение:
- `src/entities/user`: правило `fsd/insignificant-slice` отключено только для этого слайса, потому что `user` является доменной entity, даже если в текущем приложении используется только в `widgets/protected-app-layout`.
Уже устранено:
- `src/pages/dashboard` больше не является segmentless slice: real page component перенесен в `src/pages/dashboard/ui/DashboardPage.vue`;
- `src/widgets/competence-map-filters` и `src/widgets/competence-map-toolbar` слиты в `src/pages/dashboard/ui`, потому что сейчас они используются только dashboard-страницей;
- `src/stores/dashboard.ts` перенесен в `src/pages/dashboard/model/dashboardStore.ts`, `src/stores` удален;
- `src/views` удален, потому что самостоятельных потребителей не осталось;
- `src/router` перенесен в `src/app/router` и разделен на `routes.ts`, `guards.ts`, `index.ts`;
- прямые импорты generated API из прикладного кода заменены на public facade `@/shared/api`;
- `src/style.css` превращен в entrypoint, а базовые и dashboard-стили разнесены по `src/app/styles` и `src/pages/dashboard/ui/styles`.
Найденные категории:
- `src/entities/user`: слайс используется только в `widgets/protected-app-layout`; сохранен осознанно как доменная entity с точечным исключением `fsd/insignificant-slice`;
- direct imports из `@/shared/api/generated-api/...` устранены в ручном app/pages/widgets/entities коде, но это нужно закрепить lint-правилом;
- `src/shared/model/generated-zod/**` исключен как generated area;
- внутренности `src/shared/ui-kit/lib/...` конфликтуют с зарезервированными именами FSD (`lib`, `model`, `ui`), что для локального design system нужно либо исключить осознанно, либо переупаковать.
## 4. Главные проблемы текущего кода
### P0. Архитектурные нарушения, влияющие на поддержку
1. `dashboardStore.ts` уже разделен и больше не содержит ручной response cache.
Выполнено: типы, константы, фильтры, empty state, error resolving, chart data, форматтеры, сортировка, query keys и tooltip helpers вынесены в отдельные model-файлы. Store стал orchestration layer примерно на 205 строк. Ручные `analyticsResponseCache`/`filtersResponseCache` удалены; server state теперь опирается на TanStack Query cache со структурированными query keys.
2. `DashboardPage.vue` стал тоньше, но все еще выше целевого диапазона.
Выполнено: KPI, state blocks, tabs, composition panels, ranking, industries, trend, employee stats и tooltip portal вынесены в компоненты `src/pages/dashboard/ui/components`. Файл уменьшен примерно с 1712 до 487 строк. Это уже не критический монолит, но он все еще выше целевого диапазона page component 150-250 строк и требует второго прохода после декомпозиции фильтров/toolbar.
3. `CompetenceMapFilters.vue` больше не является крупным монолитом, но требует полного component-test flow.
Выполнено: draft mapping вынесен в `model/filterDraft.ts`, option definitions вынесены в `model/filterDefinitions.ts`, select field вынесен в `ui/components/CompetenceMapFilterField.vue`. `CompetenceMapFilters.vue` уменьшен примерно с 470 до 284 строк. Добавлен component test для `CompetenceMapFilterField`. Оставшийся риск: нет component tests на полный apply/reset/cancel flow и disabled/loading states диалога.
4. CSS больше не лежит целиком в root, но dashboard CSS пока разделен крупными section-файлами.
Выполнено: `src/style.css` теперь только импортирует `src/app/styles/base.css` и `src/pages/dashboard/ui/styles/dashboard.css`. Оставшийся риск: `layout.css`, `tables.css`, `composition.css`, `charts-and-industries.css` все еще крупные и используют общие class selectors. Их нужно дробить рядом с компонентами только после стабилизации UI-декомпозиции.
5. `shared/ui` очищен от прямой app/router logic в header и sidebar.
Примеры:
- выполнено: `AppHeaderActions.vue` больше не импортирует `useLogout`, а эмитит `logout`;
- выполнено: `AppHeaderBranding.vue` больше не использует router/smoothScroll напрямую, а получает props и эмитит `mainPageClick`;
- выполнено: `ProtectedAppLayout.vue` стал owner для logout redirect, route state и smooth scroll;
- выполнено: `AppSidebar` больше не импортирует router/preload/localStorage logic;
- выполнено: route matching, navigation, route preload и persisted expanded groups перенесены в `widgets/protected-app-layout/model/useProtectedAppSidebarNavigation.ts`.
Выполнено: logout orchestration больше не живет в `shared/api`; оно перенесено в `widgets/protected-app-layout/model/useProtectedLogout.ts`. В `shared/api` остались low-level endpoint facade и token primitives.
6. Public API generated API исправлен на уровне импортов, но нужна защита от регрессий.
В `src/shared/api/index.ts` добавлен facade для используемых endpoint functions и типов. Ручной прикладной код больше не импортирует `@/shared/api/generated-api/...` напрямую. Следующий шаг: закрепить это ESLint/Steiger convention и добавить README для generated area.
7. Auth page приведен ближе к FSD/Vue best practices.
Выполнено: `AuthLoginForm.vue` импортирует `useAuthLogin` из локального model-сегмента, а не из `@/pages/auth`; login schema вынесена в `pages/auth/model/loginSchema.ts`; safe redirect helper вынесен в `pages/auth/model/redirect.ts` и покрыт unit-тестом.
### P1. Риски логики и тестируемости
8. Тестовое покрытие недостаточно для безопасного рефакторинга dashboard.
Сейчас есть:
- unit-тесты для `normalizeAnalyticsFilters`, fallback association type, цветов, `toApiParams`, structured query keys, empty analytics response;
- unit-тесты для dashboard filter draft mapping и filter definitions;
- unit-тесты для chart adapters, сортировки, sort direction, pluralization/growth formatting и `escapeHtml`;
- unit-тест entrypoint UI-kit;
- E2E только для redirect на auth и локальной валидации login form.
Все еще нет тестов на:
- component behavior для dashboard cards/tables;
- component-level draft/apply/reset/cancel flow фильтров;
- tab switching;
- ranking report;
- mocked authorized dashboard load;
- визуальные состояния dashboard.
9. Loading/placeholder UX после смены фильтров не закреплен тестами.
Ручной cache layer удален в пользу TanStack Query cache. Следующий риск теперь не архитектурный cache, а UX: нужно компонентно или E2E проверить, что при смене фильтров loading/empty/fetching states остаются ожидаемыми.
10. Query key переведен со строки на структурированный объект.
Выполнено: добавлен `model/queryKeys.ts`, query keys строятся как `['competence-map-analytics', normalizeAnalyticsFilters(filters)]` и `['competence-map-analytics-filters', normalizeAnalyticsFilters(filters)]`.
11. Error handling dashboard дублирует общую error-модель.
`resolveDashboardApiErrorMessage` вынесен из store в `src/pages/dashboard/model/error.ts`, но все еще не переиспользует общую error-модель из `shared/api/error`. Следующий шаг: унифицировать, если сообщения и контракты ошибок совпадают.
12. Tooltip HTML templates строятся строками.
Сейчас есть `escapeHtml`, это правильно. Но безопасность и корректность нужно закрепить unit-тестами, потому что при будущих правках легко случайно убрать escaping.
13. `src/shared/model/generated-zod` остается generated area без ручного public API.
Steiger исключает `src/shared/model/generated-zod/**`, но для передачи заказчику нужно явно задокументировать, что это generated-код и он не редактируется вручную.
### P2. Качество сборки и tooling
14. `build` не запускает ESLint, но добавлен отдельный `check`.
`build` по-прежнему делает `typecheck && vite build`, что нормально для runtime-сборки. Для сдачи заказчику добавлен `check`, который запускает `lint`, `lint:fsd`, `typecheck`, unit tests и build. В CI отдельно стоит запускать `test:e2e` или `check:full`.
15. `vue/multi-word-component-names` полностью выключен.
Это может скрывать реальные нарушения style guide. Нужно либо включить правило и исправить компоненты, либо оставить точечные overrides для `App.vue`/generated/vendor.
16. TypeScript ослаблен.
В `tsconfig.app.json`:
- `noUnusedLocals: false`;
- `noUnusedParameters: false`;
- `erasableSyntaxOnly: false`.
Включать все сразу рискованно, но в плане нужно постепенное усиление.
17. `shared/ui-kit` исключен из ESLint и tsconfig.
Это может быть осознанно, если UI-kit является импортированным/vendor-кодом. Но перед сдачей нужно описать это в README или проверить отдельно.
18. Bundle size требует анализа.
Главный chunk 725 KB minified. Вероятные причины:
- общий UI-kit entrypoint;
- heavy chart components;
- barrel exports;
- dashboard импортирует много компонентов и данных в одном route chunk.
### P3. Навигация, naming и документация
19. Report-tab сейчас является частью dashboard.
`ReportsView.vue` перенесен в `src/pages/dashboard/ui`. Если позже появится самостоятельный route `/reports`, его нужно выделять в отдельный `pages/reports` слайс, а не держать как скрытую вкладку dashboard.
20. `docs/components/heder.css` содержит опечатку в имени `heder`.
Если эти файлы являются reference layout docs, нужно оставить как есть или переименовать с учетом ссылок.
21. Sidebar localStorage key мигрирован с legacy namespace.
Выполнено: новый ключ `analytical-panel:app-sidebar:expanded-group-ids` живет в `widgets/protected-app-layout/model/useProtectedAppSidebarNavigation.ts`; старый `mostovik:app-sidebar:expanded-group-ids` читается только как migration fallback.
22. README слишком короткий для передачи заказчику.
Нужны разделы:
- архитектура;
- команды;
- env;
- generated API;
- тестирование;
- known limitations;
- FSD rules.
## 5. Целевая FSD-структура
Цель не в том, чтобы механически создать все возможные слои, а в том, чтобы у каждой папки была понятная ответственность.
```text
src/
app/
layouts/
protected-route/
ui/
index.ts
providers/
query/
store/
router/
router/
routes.ts
guards.ts
index.ts
styles/
global.css
tokens.css
index.ts
pages/
auth/
model/
loginSchema.ts
useAuthLogin.ts
ui/
AuthPage/
AuthLoginForm/
index.ts
dashboard/
api/
competenceMapAnalyticsApi.ts
model/
filters.ts
queryKeys.ts
types.ts
useDashboardAnalytics.ts
useDashboardFilters.ts
useDashboardTabs.ts
sortRows.ts
formatters.ts
tooltip.ts
adapters.ts
ui/
DashboardPage.vue
DashboardToolbar.vue
DashboardTabs.vue
DashboardKpiGrid.vue
DashboardStatePanel.vue
DashboardSkeletonGrid.vue
DashboardAnalyticsGrid.vue
AssociationCompositionCard.vue
SezPriorityDirectionsCard.vue
TechnoparkAreaDistributionCard.vue
ClusterSpecializationsCard.vue
EmployeeStatsCard.vue
TrendCard.vue
RegionRankingCard.vue
TopIndustriesCard.vue
RegionRatingReport.vue
CompetenceMapFiltersDialog.vue
ChartTooltipPortal.vue
index.ts
widgets/
protected-app-layout/
model/
ui/
index.ts
features/
logout-session/
model/
ui/ # только если появится самостоятельная кнопка/меню
index.ts
entities/
user/
model/
index.ts
shared/
api/
client/
auth/
competence-map-analytics/
generated-api/
index.ts
config/
routes.ts
env.ts
lib/
number/
text/
navigation/
model/
generated-zod/
index.ts
ui/
app-header/ # только pure UI или перенести в widget
app-sidebar/ # только pure UI или перенести в widget
breadcrumb/
logo/
index.ts
ui-kit/
README.md
index.ts
```
Возможные корректировки после анализа переиспользования:
- `features/logout-session` можно не создавать, если logout останется только деталью `protected-app-layout`.
- `entities/user` можно оставить как доменную сущность, несмотря на warning Steiger, если ожидаются роли, профиль, права доступа и дальнейшее использование. Если это не планируется, `useCurrentUser` можно перенести ближе к `protected-app-layout`.
- `widgets/competence-map-filters` и `widgets/competence-map-toolbar` уже слиты в `pages/dashboard/ui`, потому что сейчас используются только dashboard-страницей.
- `src/shared/ui-kit` лучше трактовать как локальный design-system/vendor package. Его не нужно ломать ради буквального FSD, но нужно документировать исключение.
## 6. Детальный пошаговый план рефакторинга
### Этап 0. Зафиксировать baseline и защиту от регрессий
1. Зафиксировать текущий список проверок:
```bash
bun run lint
bun run typecheck
bun run test:unit
bun run build
bun run test:e2e
```
2. Проверить `bun run test:e2e` отдельно и зафиксировать, проходит ли он на текущей машине.
3. Добавить временный документ `docs/refactoring-regression-checklist.md` или секцию в этом файле с ручными сценариями:
- переход `/dashboard` без токена ведет на `/auth`;
- login form показывает ошибки пустых полей;
- успешный login редиректит на dashboard;
- dashboard all state;
- вкладка `ОЭЗ`;
- вкладка `Технопарки`;
- вкладка `Кластеры`;
- фильтры: открыть, выбрать, отменить, сбросить, применить;
- сортировка ranking;
- сортировка detail tables;
- region rating tab;
- loading state;
- error state;
- mobile viewport.
4. Добавить Playwright route mocks для analytics endpoints, чтобы E2E dashboard не зависел от внешнего backend.
5. Снять baseline screenshots для desktop и mobile:
- auth page;
- dashboard all;
- dashboard sez;
- dashboard technoparks;
- dashboard clusters;
- filters dialog;
- report/rating table.
6. Зафиксировать bundle baseline:
- размер `index-*.js`;
- размер route chunk `dashboard-*.js`;
- CSS sizes;
- build warnings.
### Этап 1. Настроить архитектурные проверки
Статус: выполнено для текущего объема.
1. Выполнено: Steiger добавлен в dev tooling:
```json
{
"scripts": {
"lint:fsd": "steiger src"
}
}
```
2. Выполнено: добавлен `steiger.config.ts`:
- исключить `src/shared/ui-kit/**` как локальный vendor/design-system;
- исключить generated files, но не исключать public API wrappers;
- оставить strict checks для `pages`, `widgets`, `features`, `entities`, `shared/api`, `shared/ui`.
3. Выполнено: добавлены `check` и `check:full`:
```json
{
"scripts": {
"check": "bun run lint && bun run lint:fsd && bun run typecheck && bun run test:unit && bun run build"
}
}
```
4. Выполнено: `lint:fsd` включен в `check`, потому что текущие FSD-нарушения ручного прикладного кода устранены.
5. Оставшийся план по усилению ESLint:
- убрать глобальное отключение `vue/multi-word-component-names`;
- заменить его точечными exceptions;
- добавить правила для import boundaries, если Steiger не покрывает нужные Vue-сценарии;
- добавить правило против прямых импортов из `shared/api/generated-api` вне `shared/api`.
### Этап 2. Привести `app` слой в порядок
Статус: выполнено.
1. Выполнено: `src/router/index.ts` перенесен в `src/app/router/index.ts`.
2. Выполнено: router разделен:
- `routes.ts`: route records и lazy imports;
- `guards.ts`: auth guard и title guard;
- `index.ts`: создание router.
3. Выполнено: импорт в `src/main.ts` обновлен на `./app/router`.
4. Выполнено: compatibility re-export не оставлялся, потому что импорт был единственный и безопасно обновлен.
5. Выполнено: route-level lazy loading сохранен в `routes.ts`.
6. Выполнено: app-level styles перенесены из `src/style.css` в `src/app/styles/base.css`:
- reset;
- html/body/#app;
- tokens;
- global font-family;
- минимальные общие helpers.
7. Выполнено: `src/style.css` оставлен entrypoint, который импортирует `app/styles/base.css` и dashboard CSS.
### Этап 3. Сформировать настоящий `pages/dashboard` слайс
Статус: базовое перемещение выполнено.
1. Создать сегменты:
- `src/pages/dashboard/ui`;
- `src/pages/dashboard/model` на следующем этапе;
- `src/pages/dashboard/api` при выделении dashboard API wrappers;
- `src/pages/dashboard/lib` при выделении pure helpers.
2. Выполнено: `src/views/DashboardView.vue` перенесен в `src/pages/dashboard/ui/DashboardPage.vue`.
3. Выполнено: `src/views/ReportsView.vue` перенесен в `src/pages/dashboard/ui/ReportsView.vue`, потому что это вкладка dashboard, а не самостоятельная route page.
4. Выполнено: `src/pages/dashboard/index.ts` обновлен:
```ts
export { DashboardPage } from './ui'
```
5. Выполнено: зависимость `pages/dashboard` от `src/views` удалена.
6. Выполнено: `src/views` удален, потому что других потребителей не осталось.
7. Выполнено: на этом шаге логика не дробилась, были только механические переносы и обновление импортов.
8. Проверки после шага:
```bash
bun run lint
bun run typecheck
bun run test:unit
bun run build
```
### Этап 4. Перенести и разделить dashboard model
Статус: выполнено для первого безопасного прохода.
1. Выполнено: `src/stores/dashboard.ts` перенесен в `src/pages/dashboard/model/dashboardStore.ts`.
2. Выполнено: API `useDashboardStore` сохранен, чтобы не менять всю страницу одновременно.
3. Выполнено: импорты обновлены:
- `DashboardPage.vue`;
- `ReportsView.vue`;
- `CompetenceMapFilters.vue`;
- tests.
4. Выполнено: `src/stores` удален.
5. Выполнено: `dashboardStore.ts` разделен на отдельные model-файлы:
- `types.ts`: domain/view types;
- `constants.ts`: цветовые карты и константы;
- `filters.ts`: `defaultAnalyticsFilters`, `normalizeAnalyticsFilters`, `toApiParams`, cache key;
- `emptyState.ts`: empty response factories;
- `error.ts`: dashboard error resolving;
- `chartData.ts`: chart/donut adapters;
- `formatters.ts`: number, metric, area, growth, plural forms;
- `sorting.ts`: sorting helpers;
- `tooltip.ts`: tooltip builders and escaping.
6. Выполнено: dashboard API wrappers вынесены в `src/pages/dashboard/api/competenceMapAnalyticsApi.ts`.
7. Выполнено: unit tests перенесены рядом с model и расширены до 15 dashboard-кейсов:
- filter normalization;
- filter draft mapping;
- filter definitions;
- API params;
- cache key;
- empty state;
- chart adapters;
- sorting;
- formatting/pluralization;
- tooltip escaping.
8. Оставшийся подшаг: проверить, можно ли отказаться от Pinia для dashboard.
Предпочтительный target:
- server state: TanStack Query;
- page UI state: composables/ref внутри `pages/dashboard/model`;
- Pinia оставить только если состояние нужно глобально между routes.
9. Ручной cache layer уже удален; если отказ от Pinia слишком рискованный, оставить setup store на один этап и покрыть page state/component behavior тестами.
### Этап 5. Убрать direct imports generated API
Статус: выполнен минимальный безопасный facade, осталась документация и возможная детализация entrypoints.
1. Выполнено: создан public facade `src/shared/api/index.ts`, который экспортирует используемые endpoint functions и типы.
2. Возможный следующий уровень: создать более явные public API wrappers:
```text
src/shared/api/authentication/
index.ts
src/shared/api/user/
index.ts
src/shared/api/competence-map-analytics/
index.ts
src/shared/api/generated/
README.md
```
3. В wrappers экспортировать только нужные функции и типы:
- login mutation;
- token refresh URL/handler, если нужен;
- current user endpoint;
- analytics endpoints;
- endpoint schemas/types, которые реально нужны приложению.
4. Выполнено: потребители переведены на public facade:
- `entities/user/model/useCurrentUser.ts`;
- `pages/auth/model/useAuthLogin.ts`;
- `pages/auth/ui/AuthPage/AuthLoginForm/AuthLoginForm.vue`;
- dashboard model.
5. Запретить прямые импорты из `shared/api/generated-api` вне `shared/api` через ESLint/Steiger convention.
6. Добавить README в `shared/api/generated-api`:
- generated by Orval;
- do not edit manually;
- run `bun run apigen`;
- public wrappers live one level above.
### Этап 6. Разобрать `DashboardPage.vue` на компоненты
Статус: выполнен первый безопасный проход. `DashboardPage.vue` уменьшен примерно с 1712 до 487 строк без изменения визуального поведения.
Порядок дробления шел от простого к сложному.
1. Выполнено частично: shell оставлен в `DashboardPage.vue`, tabs вынесены:
- `DashboardTabs.vue`.
2. Выполнено: KPI вынесены:
- `DashboardKpiGrid.vue`;
- отдельный `DashboardKpiCard.vue` пока не создавался, чтобы не дробить простую разметку без необходимости.
3. Выполнено: state blocks вынесены:
- `DashboardStatePanel.vue`;
- `DashboardSkeletonGrid.vue`;
- отдельные error/empty components пока не нужны.
4. Выполнено: primary cards/panels вынесены:
- `CompositionCard.vue`;
- `AssociationsCompositionPanel.vue`;
- `SezCompositionPanel.vue`;
- `TechnoparkCompositionPanel.vue`;
- `ClusterSpecializationsPanel.vue`.
5. Выполнено: secondary cards вынесены:
- `RegionRankingCard.vue`;
- `TopIndustriesCard.vue`.
6. Выполнено: trend вынесен:
- `TrendCard.vue`;
- отдельные `ClusterCreationTrendChart.vue` и `OpkParticipationTrendChart.vue` пока не создавались, потому что общий `TrendCard.vue` сохраняет связную ответственность.
7. Выполнено: employee stats вынесены:
- `EmployeeStatsCard.vue`.
8. Выполнено: tooltip portal вынесен:
- `ChartTooltipPortal.vue`;
- tooltip builders и escaping вынесены в `model/tooltip.ts`.
9. Оставшийся возможный шаг: вынести table primitives только если повторяются:
- `SortableHeaderButton.vue`;
- `AnalyticsTable.vue`;
- `RankingTable.vue`.
10. Не создавать слишком мелкие компоненты ради количества файлов. Компонент должен появляться, если:
- у него отдельная ответственность;
- его template читается независимо;
- его props API естественный;
- он уменьшает размер родителя;
- он покрывается тестом или визуально проверяется.
11. После каждого выноса:
- проверить props typing;
- не потерять `aria-*`;
- не потерять `key`;
- не изменить CSS class names до отдельного CSS шага;
- прогнать проверки.
### Этап 7. Перенести фильтры и toolbar в правильное место
Статус: ownership исправлен, первый проход внутреннего разбиения фильтров выполнен.
1. Выполнено: ownership решен в пользу dashboard page:
- `widgets/competence-map-filters` перенесен в `pages/dashboard/ui/CompetenceMapFilters.vue`;
- `widgets/competence-map-toolbar` перенесен в `pages/dashboard/ui/CompetenceMapToolbar.vue`.
2. Обоснование: фильтры и toolbar сейчас завязаны на конкретную dashboard page.
Причина: фильтры завязаны на конкретные `AnalyticsFilters`, `dashboard.filtersResponse`, `dashboard.applyFilters`.
3. Выполнено: логика фильтров разделена:
- `filterDraft.ts`: draft mapping и nullable conversion;
- `filterDefinitions.ts`: option definitions;
- `CompetenceMapFilterField.vue`: select field UI.
4. Выполнено: `competence-map-toolbar` перенесен в dashboard page, потому что он только про breadcrumb этой страницы.
5. Если toolbar станет общим, сделать pure shared component:
- props: `breadcrumbItems`;
- no dashboard imports;
- no route assumptions.
### Этап 8. Очистить `shared/ui` от app/business logic
Статус: `AppHeader` и `AppSidebar` очищены в первом проходе.
1. Выполнено: `AppHeader` разделен по ответственности:
- `shared/ui/app-header`: только layout, slots, display props;
- `widgets/protected-app-layout/ui`: current user, logout, router callbacks.
2. Выполнено: logout orchestration перенесен из `shared/api`:
Возможные варианты:
- текущий выбранный вариант: `widgets/protected-app-layout/model/useProtectedLogout`;
- `features/logout-session` не создавался, потому что Steiger корректно считает отдельный feature slice преждевременным при одном потребителе;
- token primitives и low-level logout endpoint facade остались в `shared/api`.
Если logout начнет переиспользоваться несколькими flows, тогда его можно поднять в `features/logout-session`.
3. Оставить token primitives в `shared/api`, потому что API client и refresh interceptor используют их глобально.
4. Выполнено: `AppSidebar` сделан более чистым:
- route-specific preload убран из shared component;
- callbacks на click/hover передаются из widget owner;
- route matching/navigation строятся в widget model.
5. Выполнено: localStorage key перенесен из shared UI в widget model.
6. Выполнено: `mostovik:app-sidebar:expanded-group-ids` исправлен миграционно:
- новый ключ: `analytical-panel:app-sidebar:expanded-group-ids`;
- при чтении есть fallback на старый ключ, чтобы не ломать существующее состояние.
### Этап 9. Привести auth page к FSD и Vue best practices
Статус: выполнено для первого безопасного прохода.
1. Выполнено: schema перенесена из `AuthLoginForm.vue` в `pages/auth/model/loginSchema.ts`.
2. Выполнено: self-import убран:
- было: `import { useAuthLogin } from '@/pages/auth'`;
- стало: импорт из локального model-сегмента.
3. Выполнено: тип `Login` импортируется через public facade `@/shared/api`, а не generated path.
4. Выполнено: redirect safety вынесен в `pages/auth/model/redirect.ts`:
- текущая проверка `/` и не `//` правильная;
- добавлен unit test на внешний/protocol-relative redirect.
5. Улучшить submit flow:
- оставить защиту от double submit;
- проверить disabled state для username input тоже, если UX требует;
- оставить accessible error messages.
6. Выполнено частично:
- empty validation;
- whitespace validation;
- safe redirect.
7. Оставшийся подшаг:
- API error message;
- full submit flow component/E2E tests.
### Этап 10. Перенести и изолировать CSS
Статус: первый безопасный перенос выполнен, компонентная изоляция осталась.
1. Выполнено: `src/style.css` разделен:
- `src/app/styles/base.css`;
- `src/pages/dashboard/ui/styles/dashboard.css`;
- `src/pages/dashboard/ui/styles/sections/layout.css`;
- `src/pages/dashboard/ui/styles/sections/composition.css`;
- `src/pages/dashboard/ui/styles/sections/tables.css`;
- `src/pages/dashboard/ui/styles/sections/charts-and-industries.css`;
- `src/pages/dashboard/ui/styles/sections/responsive.css`.
2. Выполнено: dashboard-specific CSS перенесен без переписывания selectors.
3. Следующий шаг: распределить CSS по компонентам:
- tabs styles рядом с `DashboardTabs.vue`;
- KPI styles рядом с `DashboardKpiGrid.vue`;
- card styles рядом с конкретными cards;
- table styles рядом с table components;
- tooltip styles рядом с `ChartTooltipPortal.vue`;
- reports styles рядом с `RegionRatingReport.vue`.
4. Избегать изменения class names одновременно с переносом template.
5. Если используется `:deep`, оставить комментарий только там, где это действительно override UI-kit internals.
6. Проверить mobile breakpoints после каждого крупного переноса.
### Этап 11. Улучшить server state и dashboard data model
Статус: первый проход выполнен.
1. Выполнено: query key factories вынесены:
```ts
export const competenceMapAnalyticsQueryKeys = {
all: ['competence-map-analytics'] as const,
list: (filters: AnalyticsFilters) =>
[...competenceMapAnalyticsQueryKeys.all, normalizeAnalyticsFilters(filters)] as const,
filters: (filters: AnalyticsFilters) =>
['competence-map-analytics-filters', normalizeAnalyticsFilters(filters)] as const,
}
```
2. Проверить сериализуемость фильтров и отсутствие `undefined`.
3. Выполнено: `createAnalyticsCacheKey` удален, строковый key больше не используется.
4. Выполнено: ручной response cache удален, server state опирается на Vue Query cache.
5. Оставшийся подшаг: решить, как показывать прошлые данные при смене фильтров:
- использовать Vue Query cache;
- использовать `placeholderData`, если нужно;
- явно покрыть loading UX тестами.
6. Перенести `resolveApiErrorMessage` dashboard на общий `shared/api/error`.
7. Вынести DTO-to-view adapters:
- `toCompositionDonutData`;
- `toTrendChartData`;
- `toRegionReportRows`;
- `toEmployeeStatItems`.
8. Добавить unit tests на все adapters.
### Этап 12. Оптимизировать public APIs и barrel exports
1. Проверить все `index.ts`:
- public API должен экспортировать только нужный контракт;
- не использовать `export *` для всего подряд, если это открывает internals;
- внутри слайса не импортировать из собственного public API.
2. Для `shared/ui` рассмотреть subpath imports:
- `@/shared/ui/breadcrumb`;
- `@/shared/ui/logo`;
- `@/shared/ui/app-header`;
- `@/shared/ui/app-sidebar`.
3. Для `shared/ui-kit` оценить:
- оставить единый `@gisp/ui-kit-shadcn-vue`, если tree-shaking нормальный;
- либо добавить component-level entrypoints, если bundle analysis подтвердит проблему.
4. Добавить architecture notes в `src/shared/ui-kit/lib/README.md` или `src/shared/ui-kit/README.md`:
- почему папка исключена из FSD lint;
- как импортировать компоненты;
- как добавлять новые компоненты;
- как не ломать public API.
### Этап 13. Усилить тесты
1. Unit tests:
- `normalizeAnalyticsFilters`;
- `toApiParams`;
- query keys;
- sortRows with null/string/number/tie by rank;
- formatNumber;
- formatMetricValue;
- getRuPluralForm;
- formatOrganizationsText;
- escapeHtml;
- tooltip builders;
- dashboard adapters;
- auth schema;
- safe redirect helper.
2. Component tests:
- DashboardTabs emits/apply behavior;
- CompetenceMapFiltersDialog draft/apply/reset/cancel;
- RegionRankingCard sorting UI;
- TopIndustriesCard empty and filled states;
- ChartTooltipPortal visibility.
3. E2E tests:
- auth redirect;
- login validation;
- mocked authorized dashboard load;
- tab switch all/sez/technoparks/clusters;
- filter dialog;
- sort buttons;
- region rating tab;
- mobile layout smoke.
4. Visual regression:
- either Playwright screenshots with controlled mock data;
- or at minimum saved screenshots before/after refactor during manual validation.
5. Accessibility checks:
- role/aria для tables;
- focusable chart segments;
- keyboard focus in dialog;
- `aria-busy`;
- tooltips not blocking focus.
### Этап 14. Bundle/performance pass
1. Добавить bundle analyzer или Vite visualizer.
2. Сравнить:
- before/after main chunk;
- dashboard route chunk;
- UI-kit chunk;
- chart chunk.
3. Проверить, почему `index-*.js` 725 KB:
- общий import UI-kit;
- route preload;
- shared barrel;
- chart components;
- TanStack table/chart libraries.
4. Возможные оптимизации:
- лениво грузить тяжелые chart cards;
- разделить dashboard report tab;
- component-level imports из UI-kit;
- manual chunks для UI-kit/charts;
- отложенная загрузка report table.
5. Не применять оптимизации до архитектурного переноса, чтобы не смешивать причины регрессий.
### Этап 15. Документация для передачи заказчику
1. Расширить README:
- стек;
- требования к Node/Bun;
- env variables;
- запуск dev;
- build/preview;
- tests;
- generated API;
- FSD structure;
- deployment notes;
- known warnings.
2. Добавить `docs/architecture.md`:
- слои;
- правила импортов;
- public API;
- где хранить API;
- где хранить UI;
- как добавлять новую страницу;
- как добавлять новый analytics block.
3. Добавить `docs/testing.md`:
- unit;
- e2e;
- visual checks;
- mock data policy.
4. Добавить `docs/generated-api.md`:
- Orval config;
- OpenAPI source;
- команда `bun run apigen`;
- что нельзя редактировать руками.
## 7. Предлагаемый порядок выполнения PR/коммитов
1. Tooling and baseline:
- выполнено: добавить `lint:fsd`/Steiger config;
- выполнено: добавить `check` и `check:full`;
- добавить dashboard E2E mocks skeleton;
- выполнено: runtime не менялся на этом шаге.
2. App/router cleanup:
- выполнено: перенести router в app;
- выполнено: разделить routes/guards;
- выполнено: проверки.
3. Dashboard page relocation:
- выполнено: `views/DashboardView.vue` -> `pages/dashboard/ui/DashboardPage.vue`;
- выполнено: `views/ReportsView.vue` -> `pages/dashboard/ui/ReportsView.vue`;
- выполнено: dashboard-only widgets перенесены в `pages/dashboard/ui`;
- выполнено: `src/views` удален;
- выполнено: проверки.
4. Dashboard model relocation:
- выполнено: `stores/dashboard.ts` -> `pages/dashboard/model/dashboardStore.ts`;
- выполнено: model split на filters/constants/types/chartData/formatters/sorting/tooltip/error/emptyState;
- выполнено: API wrappers в `pages/dashboard/api`;
- выполнено: временно сохранен `useDashboardStore` API;
- выполнено: удален `src/stores`;
- выполнено: dashboard unit-тесты расширены до 15 кейсов;
- выполнено: проверки.
5. Public API wrappers:
- выполнено: shared API facade;
- выполнено: убрать generated direct imports из ручного прикладного кода;
- выполнено: исправить Steiger public-api issues в текущем объеме;
- выполнено: проверки.
6. Auth cleanup:
- выполнено: schema в model;
- выполнено: убрать self-import;
- выполнено: тесты redirect/schema;
- выполнено: проверки.
7. Dashboard pure model extraction:
- выполнено: filters/sort/formatters/tooltip/adapters;
- выполнено: queryKeys заменены на структурированные ключи;
- выполнено: unit tests;
- выполнено: проверки.
8. Dashboard UI decomposition:
- выполнено: карточки, панели, tooltip portal, tabs, states;
- выполнено: фильтры, draft helpers и select field;
- выполнено: проверки после блока.
9. Shared UI cleanup:
- выполнено частично: AppHeader стал ближе к pure UI;
- выполнено: AppSidebar стал pure UI;
- выполнено: logout orchestration перенесен в widget model;
- выполнено: protected layout composes header behavior;
- выполнено: проверки.
10. CSS relocation:
- выполнено: global split;
- выполнено: dashboard CSS split;
- screenshots;
- выполнено: проверки.
11. Bundle/performance:
- analyzer;
- chunk strategy;
- UI-kit import strategy;
- проверки.
12. Final docs and handoff:
- README;
- architecture docs;
- generated API docs;
- final full check.
## 8. Definition of Done
Рефакторинг можно считать завершенным, когда:
- `src/views` удален;
- `src/stores` удален или обоснован как отдельный слой не нужен;
- `src/router` перенесен в `app`;
- `pages/dashboard` имеет реальные сегменты `ui`, `model`, `api`;
- `DashboardPage.vue` стал тонкой композицией, а не файлом на 1700 строк;
- dashboard pure logic покрыта unit-тестами;
- generated API не импортируется напрямую из app/pages/widgets/entities;
- `shared/ui` не содержит бизнес-логики logout/current user/router-specific behavior;
- `src/style.css` больше не содержит весь dashboard;
- Steiger либо проходит, либо имеет задокументированные исключения для generated/vendor UI-kit;
- `bun run lint` проходит;
- `bun run typecheck` проходит;
- `bun run test:unit` проходит;
- `bun run build` проходит;
- `bun run test:e2e` проходит;
- визуальные screenshots ключевых dashboard states совпадают или изменения явно согласованы;
- README и архитектурная документация достаточны для передачи заказчику.
## 9. Что не делать на первом проходе
- Не менять дизайн, размеры, цвета и сетку без отдельного согласования.
- Не менять backend contract и названия query params.
- Не удалять Pinia одномоментно, если это смешает слишком много причин регрессий.
- Не переписывать UI-kit внутренности до bundle/performance этапа.
- Не добавлять `features` ради формального наличия слоя.
- Не делать массовое переименование CSS classes одновременно с переносом компонентов.
- Не включать все strict TS flags одним коммитом.
## 10. Статус первого большого refactor-pass
Защитная инфраструктура, FSD-перенос и первая декомпозиция выполнены:
1. Добавлен Steiger script/config с осознанными исключениями для generated/vendor-зон.
2. Добавлены `check` и `check:full`.
3. Добавлены file-size thresholds и FSD policy в `docs/review.md`.
4. Router перенесен в `src/app/router` и разделен на `routes.ts`, `guards.ts`, `index.ts`.
5. Dashboard page перенесен из `src/views` в `src/pages/dashboard/ui`.
6. `ReportsView` перенесен в dashboard page slice как вкладка, а не самостоятельная route page.
7. Dashboard-only widgets перенесены из `src/widgets` в `src/pages/dashboard/ui`.
8. Dashboard store перенесен из `src/stores` в `src/pages/dashboard/model`.
9. Dashboard model разделен на типы, константы, фильтры, empty state, error, chart data, форматтеры, сортировку и tooltip helpers.
10. Dashboard API wrappers вынесены в `src/pages/dashboard/api`.
11. Dashboard UI разделен на композиционные компоненты в `src/pages/dashboard/ui/components`.
12. `src/style.css` превращен в entrypoint, dashboard CSS разнесен по section-файлам.
13. Generated API imports закрыты public facade `@/shared/api`.
14. `AppHeader` очищен от прямого router/logout ownership, поведение перенесено в `ProtectedAppLayout`.
15. Фильтры dashboard разделены на `filterDraft.ts`, `filterDefinitions.ts` и `CompetenceMapFilterField.vue`.
16. `AppSidebar` очищен от router/preload/localStorage ownership; поведение перенесено в `useProtectedAppSidebarNavigation`.
17. Sidebar storage key мигрирован на `analytical-panel:app-sidebar:expanded-group-ids` с fallback на legacy key.
18. Dashboard server state переведен на структурированные Vue Query keys.
19. Ручной `analyticsResponseCache`/`filtersResponseCache` удален.
20. Auth schema и safe redirect helper вынесены в `pages/auth/model`.
21. Logout orchestration перенесен из `shared/api` в `widgets/protected-app-layout/model/useProtectedLogout.ts`.
22. Handoff-документация добавлена: `docs/architecture.md`, `docs/generated-api.md`, `docs/testing.md`, расширен `README.md`.
23. Unit/component tests расширены до 18 кейсов.
24. `bun run lint:fsd` проходит без ошибок.
Следующий безопасный этап:
1. Добавить полный component-test flow для `CompetenceMapFilters.vue`:
- open/sync draft;
- apply;
- reset;
- cancel;
- disabled/loading state.
2. Закрепить dashboard loading/placeholder UX тестами после удаления ручного cache layer.
3. Продолжить CSS-изоляцию:
- разнести `layout.css`, `tables.css`, `composition.css`, `charts-and-industries.css` ближе к компонентам;
- не менять class names одновременно с переносом.
4. Добавить dashboard E2E с mocked authorized state и визуальные screenshots для desktop/mobile.