diff --git a/apps/docs/docs/adr/ADR-001-backend-single-point-of-access.md b/apps/docs/docs/adr/ADR-001-backend-single-point-of-access.md index 6170454..0321030 100644 --- a/apps/docs/docs/adr/ADR-001-backend-single-point-of-access.md +++ b/apps/docs/docs/adr/ADR-001-backend-single-point-of-access.md @@ -1,13 +1,13 @@ -# ADR-001: Backend — Single Point of Access to MOEX +# ADR-001: Backend — единственная точка доступа к MOEX -**Status:** Accepted -**Date:** 2026-06-13 -**Deciders:** Architect, Tech Lead +**Статус:** Accepted +**Дата:** 2026-06-13 +**Участники решения:** Architect, Tech Lead -## Context +## Контекст Frontend должен отображать данные Московской биржи. MOEX ISS API отдаёт сырые данные со сложной структурой (вложенные таблицы, различные форматы). Прямые запросы с фронта приведут к дублированию логики нормализации, усложнят обработку ошибок и сделают систему зависимой от внешнего API. -## Decision +## Решение Backend (NestJS) является единственной точкой доступа к MOEX. Frontend никогда не обращается к MOEX напрямую. Backend: @@ -17,7 +17,7 @@ Backend: - Обрабатывает ошибки MOEX (пустые данные, rate limit, таймауты) - Предоставляет собственный OpenAPI-контракт для фронта -## Consequences +## Последствия - Единый источник правды для трансформации данных - Изоляция изменений MOEX API — меняется только MoexClient - Централизованное кеширование сокращает количество запросов к MOEX diff --git a/apps/docs/docs/adr/ADR-002-in-memory-cache.md b/apps/docs/docs/adr/ADR-002-in-memory-cache.md index 62175ff..216f1fe 100644 --- a/apps/docs/docs/adr/ADR-002-in-memory-cache.md +++ b/apps/docs/docs/adr/ADR-002-in-memory-cache.md @@ -1,13 +1,13 @@ -# ADR-002: In-Memory Cache with Migration Path to Redis +# ADR-002: In-memory cache с путём миграции на Redis -**Status:** Accepted -**Date:** 2026-06-13 -**Deciders:** Architect, Tech Lead +**Статус:** Accepted +**Дата:** 2026-06-13 +**Участники решения:** Architect, Tech Lead -## Context +## Контекст Для MVP требуется кеширование MOEX-данных, чтобы снизить нагрузку на внешнее API и обеспечить приемлемое время ответа. На начальном этапе нет требований к горизонтальному масштабированию, и хочется избежать внешних зависимостей. -## Decision +## Решение Использовать `@nestjs/cache-manager` с MemoryStore. TTL настраивается per-endpoint через конфигурацию. Архитектура позволяет переключиться на Redis заменой импорта провайдера: @@ -26,7 +26,7 @@ CacheModule.registerAsync({ }) ``` -## Consequences +## Последствия - Нет внешних зависимостей для MVP - Кеш сбрасывается при рестарте сервера (приемлемо для read-only приложения) - Чистый путь миграции на Redis diff --git a/apps/docs/docs/adr/ADR-003-rate-limiting-strategy.md b/apps/docs/docs/adr/ADR-003-rate-limiting-strategy.md index 483a88f..f007be9 100644 --- a/apps/docs/docs/adr/ADR-003-rate-limiting-strategy.md +++ b/apps/docs/docs/adr/ADR-003-rate-limiting-strategy.md @@ -1,20 +1,20 @@ -# ADR-003: Rate Limiting Strategy for MOEX Client +# ADR-003: Стратегия rate limiting для MOEX client -**Status:** Accepted -**Date:** 2026-06-13 -**Deciders:** Architect, Tech Lead +**Статус:** Accepted +**Дата:** 2026-06-13 +**Участники решения:** Architect, Tech Lead -## Context +## Контекст MOEX ISS не документирует жёсткие лимиты на количество запросов, но массовые запросы могут привести к блокировке или ухудшению качества обслуживания. Backend является единственным клиентом MOEX и должен контролировать исходящий трафик. -## Decision +## Решение Внедрить два механизма в MoexClient: -1. **Request Queue (p-queue)**: конфигурируемый лимит запросов в секунду (default: 10 req/s). Запросы сверх лимита ставятся в очередь и выполняются по расписанию. +1. **Request queue (p-queue)**: конфигурируемый лимит запросов в секунду (по умолчанию: 10 req/s). Запросы сверх лимита ставятся в очередь и выполняются по расписанию. 2. **Circuit Breaker (`@nestjs/axios` + interceptor)**: при 5+ последовательных ошибках (5xx, timeout, network error) клиент перестаёт отправлять запросы к MOEX на 30 секунд. После таймаута — пробный запрос для восстановления. -## Consequences +## Последствия - Плавная нагрузка на MOEX, без пиков - Автоматическое восстановление после сбоев MOEX - Graceful degradation: при отключённом circuit breaker возвращаются кешированные данные diff --git a/apps/docs/docs/adr/ADR-004-feature-modules.md b/apps/docs/docs/adr/ADR-004-feature-modules.md index ab31eec..804d65a 100644 --- a/apps/docs/docs/adr/ADR-004-feature-modules.md +++ b/apps/docs/docs/adr/ADR-004-feature-modules.md @@ -1,16 +1,16 @@ -# ADR-004: Feature Modules by Domain +# ADR-004: Feature-модули по доменам -**Status:** Accepted -**Date:** 2026-06-13 -**Deciders:** Architect, Tech Lead +**Статус:** Accepted +**Дата:** 2026-06-13 +**Участники решения:** Architect, Tech Lead -## Context +## Контекст NestJS рекомендует модульную архитектуру. Требования указывают на архитектуру по feature modules. Модули должны иметь чёткие границы и быть тестируемыми изолированно. -## Decision +## Решение Каждый бизнес-домен — отдельный NestJS feature module: -| Module | Responsibility | +| Модуль | Ответственность | |--------|---------------| | `MoexClientModule` | HTTP-клиент к MOEX ISS, rate limiting, circuit breaker | | `CacheModule` | Абстракция кеширования | @@ -22,7 +22,7 @@ NestJS рекомендует модульную архитектуру. Тре Каждый module exports свой сервис, control imports через `@Module({ imports: [...] })`. -## Consequences +## Последствия - Чёткие границы, изолированное тестирование - Возможность вынести модуль в отдельный микросервис - Понятная навигация по коду diff --git a/apps/docs/docs/adr/ADR-005-openapi-codegen-frontend.md b/apps/docs/docs/adr/ADR-005-openapi-codegen-frontend.md index 244b0ad..7175666 100644 --- a/apps/docs/docs/adr/ADR-005-openapi-codegen-frontend.md +++ b/apps/docs/docs/adr/ADR-005-openapi-codegen-frontend.md @@ -1,13 +1,13 @@ -# ADR-005: OpenAPI Codegen with openapi-typescript +# ADR-005: OpenAPI codegen через openapi-typescript -**Status:** Accepted -**Date:** 2026-06-13 -**Deciders:** Architect, Tech Lead +**Статус:** Accepted +**Дата:** 2026-06-13 +**Участники решения:** Architect, Tech Lead -## Context +## Контекст Frontend должен потреблять API бэкенда. Ручное написание клиентов и DTO приводит к рассинхронизации с бэкендом и ошибкам типизации. -## Decision +## Решение Использовать `openapi-typescript` + `openapi-fetch` для генерации: - TypeScript типов (DTO, request/response schemas) @@ -33,7 +33,7 @@ export function useStock(secid: string) { } ``` -## Consequences +## Последствия - Полная типобезопасность на стыке frontend/backend - Автоматическая синхронизация с API-контрактом - TanStack Query hooks пишутся вручную — полный контроль staleTime/caching diff --git a/apps/docs/docs/adr/ADR-006-no-cci.md b/apps/docs/docs/adr/ADR-006-no-cci.md index ab2f9f5..56a7b62 100644 --- a/apps/docs/docs/adr/ADR-006-no-cci.md +++ b/apps/docs/docs/adr/ADR-006-no-cci.md @@ -1,20 +1,20 @@ -# ADR-006: CCI (Financial Reporting) Moved Out of MVP +# ADR-006: CCI (финансовая отчётность) вынесен за пределы MVP -**Status:** Accepted -**Date:** 2026-06-13 -**Deciders:** Architect, Product +**Статус:** Accepted +**Дата:** 2026-06-13 +**Участники решения:** Architect, Product -## Context +## Контекст MOEX предоставляет корпоративную информацию (CCI) — финансовую отчётность по МСФО/РСБУ. Данные включают отчёты о прибылях/убытках, балансовые отчёты, мультипликаторы. Однако: - CCI API имеет собственную сложную структуру (виды отчётности, периоды, индикаторы) - Данные требуют дополнительной нормализации и расчёта метрик - Для MVP пользователи хотят базовую информацию (цена, купон, график) -## Decision +## Решение Не включать CCI в MVP. Roadmap на post-MVP. -## Consequences +## Последствия - Меньший объём работы в MVP - API не привязывается к CCI-схемам (будет отдельный модуль) - Пользователи не увидят мультипликаторы (P/E, EV/EBITDA) в первой версии diff --git a/apps/docs/docs/adr/ADR-007-two-level-caching.md b/apps/docs/docs/adr/ADR-007-two-level-caching.md index 6826570..50ad4c0 100644 --- a/apps/docs/docs/adr/ADR-007-two-level-caching.md +++ b/apps/docs/docs/adr/ADR-007-two-level-caching.md @@ -1,15 +1,15 @@ -# ADR-007: Two-Level Caching (Backend + Frontend) +# ADR-007: Двухуровневое кеширование (backend + frontend) -**Status:** Accepted -**Date:** 2026-06-13 -**Deciders:** Architect +**Статус:** Accepted +**Дата:** 2026-06-13 +**Участники решения:** Architect -## Context +## Контекст Данные MOEX имеют задержку 15 минут. Кеширование на одном уровне (только бэкенд или только фронтенд) неоптимально: - Только бэкенд: каждый пользователь создаёт запрос к серверу - Только фронтенд: нет централизованного кеша, не защищает MOEX от повторных запросов -## Decision +## Решение Внедрить два уровня кеширования: 1. **Backend (in-memory cache-manager)**: централизованное кеширование ответов от MOEX. Предотвращает повторные запросы к MOEX от разных пользователей. @@ -20,7 +20,7 @@ TTL согласованы (см. Caching Strategy). Cache-Control заголовки в HTTP-ответах для промежуточных proxy/CDN (опционально). -## Consequences +## Последствия - Избыточность intentional: resilience при отказе одного уровня - TanStack Query staleTime = backend TTL (нет лишних запросов) - При рестарте бэкенда фронт всё ещё имеет данные в memory cache diff --git a/apps/docs/docs/adr/ADR-008-auth-system.md b/apps/docs/docs/adr/ADR-008-auth-system.md index d94e80b..f6e45ab 100644 --- a/apps/docs/docs/adr/ADR-008-auth-system.md +++ b/apps/docs/docs/adr/ADR-008-auth-system.md @@ -3,13 +3,13 @@ sidebar_position: 14 title: ADR-008 --- -# ADR-008: Authentication & Authorization System +# ADR-008: Система Authentication и Authorization -## Status +## Статус Accepted -## Context +## Контекст Приложение MoexVibe публичное, но для персонализации и будущих функций (избранное, уведомления, подписки) требуется система аутентификации. Необходимо решение, которое: @@ -18,26 +18,26 @@ Accepted - Позволяет расширять ролевую модель - Интегрируется в существующий стек (NestJS + React) -## Decision +## Решение ### Backend -- **Database:** SQLite через Prisma ORM (без необходимости внешнего сервиса) -- **Auth strategy:** JWT access tokens (15m) + refresh tokens (7d) in httpOnly cookies -- **Password storage:** bcrypt with 12 salt rounds -- **Guard model:** Global `JwtAuthGuard` (все маршруты защищены по умолчанию, `@Public()` для открытых) +- **База данных:** SQLite через Prisma ORM без внешнего сервиса +- **Auth strategy:** JWT access tokens (15m) + refresh tokens (7d) в httpOnly cookies +- **Хранение паролей:** bcrypt with 12 salt rounds +- **Guard model:** глобальный `JwtAuthGuard` (все маршруты защищены по умолчанию, `@Public()` для открытых) - **RBAC:** Роли `user` и `admin`, проверка через `RolesGuard` ### Frontend -- **State management:** React Context (`AuthContext`) для хранения пользователя и access token -- **Token storage:** Access token только в памяти (не localStorage), refresh token в httpOnly cookie +- **Управление состоянием:** React Context (`AuthContext`) для хранения пользователя и access token +- **Token storage:** access token только в памяти (не localStorage), refresh token в httpOnly cookie - **Auto-refresh:** При 401 → автоматический вызов `/auth/refresh` → повтор оригинального запроса -- **Route protection:** `ProtectedRoute` компонент, редирект на `/login` с return URL +- **Route protection:** компонент `ProtectedRoute`, редирект на `/login` с return URL -## Consequences +## Последствия -### Positive +### Плюсы - httpOnly cookie защищает refresh token от XSS - Access token в памяти защищён от кражи через localStorage @@ -45,17 +45,17 @@ Accepted - Refresh token rotation повышает безопасность - Глобальный guard — безопасность по умолчанию -### Negative +### Минусы - Access token теряется при полной перезагрузке страницы (восстанавливается через refresh cookie) - Для production требуется генерация надёжных JWT_SECRET -- Нет rate limiting на login endpoint (TODO) +- Rate limiting на login endpoint не входит в текущее решение - Нет верификации email (принятое упрощение) -## Migration Path +## Путь миграции Для перехода на PostgreSQL потребуется: 1. Изменить `provider` в schema.prisma на `postgresql` 2. Обновить `DATABASE_URL` 3. Выполнить `prisma migrate dev` -4. Никаких изменений кода не требуется (Prisma abstracts database) +4. Никаких изменений кода не требуется: Prisma абстрагирует database diff --git a/apps/docs/docs/adr/ADR-009-portfolio-domain.md b/apps/docs/docs/adr/ADR-009-portfolio-domain.md index 010a8bc..78058ef 100644 --- a/apps/docs/docs/adr/ADR-009-portfolio-domain.md +++ b/apps/docs/docs/adr/ADR-009-portfolio-domain.md @@ -1,27 +1,29 @@ -# ADR-009: Portfolio Domain Model +# ADR-009: Доменная модель портфеля -**Status:** Accepted +**Статус:** Accepted -**Date:** 2026-06-14 +**Дата:** 2026-06-14 -## Context +## Контекст -Adding portfolio tracking feature to MoexVibe. Need to decide on data model — relational tables vs document-oriented storage for portfolio and position data. +В MoexVibe добавляется отслеживание портфелей. Нужно выбрать модель данных: реляционные таблицы +или document-oriented storage для портфелей и позиций. -## Decision +## Решение -Use SQL via Prisma (existing database) with `Portfolio` and `Position` as separate tables. JSON fields for flexible data (`targets`, `tags`). +Использовать SQL через Prisma в существующей базе: `Portfolio` и `Position` как отдельные таблицы, +JSON-поля для гибких данных (`targets`, `tags`). -## Rationale +## Обоснование -- SQLite already in use for User model -- Portfolio and Position have clear relational structure (1:N) -- JSON fields cover semi-structured requirements (targets, tags) without adding another database -- No need for complex queries across tags at current scale (50 positions max per portfolio) -- Straightforward migration to PostgreSQL if needed +- SQLite уже используется для модели User +- Portfolio и Position имеют понятную реляционную структуру 1:N +- JSON-поля закрывают полуструктурированные требования (`targets`, `tags`) без новой базы +- На текущем масштабе не нужны сложные запросы по tags (до 50 позиций на портфель) +- При необходимости есть прямой путь миграции на PostgreSQL -## Consequences +## Последствия -- JSON fields not individually queryable in SQLite (acceptable for Phase 1) -- Targets edited as full JSON replacement (not atomic per-item update) -- Cascade delete handles portfolio → position cleanup +- JSON-поля не индексируются и не запрашиваются по отдельности в SQLite, что приемлемо для Phase 1 +- Targets редактируются полной заменой JSON, без атомарного обновления отдельных элементов +- Cascade delete очищает позиции при удалении портфеля diff --git a/apps/docs/docs/adr/ADR-010-backend-price-computation.md b/apps/docs/docs/adr/ADR-010-backend-price-computation.md index 6898ee2..90efa6e 100644 --- a/apps/docs/docs/adr/ADR-010-backend-price-computation.md +++ b/apps/docs/docs/adr/ADR-010-backend-price-computation.md @@ -1,26 +1,29 @@ -# ADR-010: Backend Price Computation +# ADR-010: Расчёт цен на backend -**Status:** Accepted +**Статус:** Accepted -**Date:** 2026-06-14 +**Дата:** 2026-06-14 -## Context +## Контекст -Portfolio positions need current market prices for value calculation. Where should price computation happen — on backend or frontend? +Позициям портфеля нужны текущие рыночные цены для расчёта стоимости. Нужно решить, где выполнять +расчёт: на backend или frontend. -## Decision +## Решение -Compute prices on backend. `GET /api/v1/portfolios/:id` returns fully computed `PortfolioDetailResponseDto` with `currentPrice`, `currentValue`, `weightPercent`, and `deviation` for each position. +Рассчитывать цены на backend. `GET /api/v1/portfolios/:id` возвращает полностью рассчитанный +`PortfolioDetailResponseDto` с `currentPrice`, `currentValue`, `weightPercent` и `deviation` для +каждой позиции. -## Rationale +## Обоснование -- Single source of truth for financial calculations -- Frontend receives ready-to-display data -- Backend caching reduces MOEX API calls -- Avoids N individual price requests from frontend +- Единый источник правды для финансовых расчётов +- Frontend получает готовые к отображению данные +- Backend cache сокращает количество запросов к MOEX API +- Frontend не делает N отдельных запросов цен -## Consequences +## Последствия -- Backend makes N MOEX requests per portfolio read (cached by TTL 900s) -- Portfolio endpoint cannot use naive response caching (prices per-user) -- Extra load on backend when many users view portfolios simultaneously +- Backend делает N запросов к MOEX при чтении портфеля; они кешируются с TTL 900s +- Portfolio endpoint не может использовать наивное response caching, потому что цены считаются для пользователя +- При одновременном просмотре портфелей многими пользователями растёт нагрузка на backend diff --git a/apps/docs/docs/adr/index.md b/apps/docs/docs/adr/index.md index 1e994b0..7fea488 100644 --- a/apps/docs/docs/adr/index.md +++ b/apps/docs/docs/adr/index.md @@ -1,16 +1,16 @@ -# Architecture Decision Records +# Архитектурные решения (ADR) -| ADR | Status | Description | +| ADR | Статус | Описание | |---|---|---| -| [ADR-001](ADR-001-backend-single-point-of-access) | Accepted | Backend — Single Point of Access to MOEX | -| [ADR-002](ADR-002-in-memory-cache) | Accepted | In-Memory Cache Strategy | -| [ADR-003](ADR-003-rate-limiting-strategy) | Accepted | Rate Limiting Strategy | -| [ADR-004](ADR-004-feature-modules) | Accepted | Feature Modules Architecture | -| [ADR-005](ADR-005-openapi-codegen-frontend) | Accepted | OpenAPI Codegen for Frontend | -| [ADR-006](ADR-006-no-cci) | Deprecated | No Custom Components Infrastructure | -| [ADR-007](ADR-007-two-level-caching) | Draft | Two-Level Caching (In-Memory + Redis) | -| [ADR-008](ADR-008-auth-system) | Accepted | Authentication & Authorization | -| [ADR-009](ADR-009-portfolio-domain) | Accepted | Portfolio Domain Model | -| [ADR-010](ADR-010-backend-price-computation) | Accepted | Backend Price Computation | +| [ADR-001](ADR-001-backend-single-point-of-access) | Accepted | Backend — единственная точка доступа к MOEX | +| [ADR-002](ADR-002-in-memory-cache) | Accepted | Стратегия in-memory cache | +| [ADR-003](ADR-003-rate-limiting-strategy) | Accepted | Стратегия rate limiting | +| [ADR-004](ADR-004-feature-modules) | Accepted | Архитектура feature-модулей | +| [ADR-005](ADR-005-openapi-codegen-frontend) | Accepted | OpenAPI codegen для frontend | +| [ADR-006](ADR-006-no-cci) | Deprecated | CCI вынесен за пределы MVP | +| [ADR-007](ADR-007-two-level-caching) | Draft | Двухуровневый cache: backend + frontend | +| [ADR-008](ADR-008-auth-system) | Accepted | Authentication и Authorization | +| [ADR-009](ADR-009-portfolio-domain) | Accepted | Доменная модель портфеля | +| [ADR-010](ADR-010-backend-price-computation) | Accepted | Расчёт цен на backend | Все опубликованные ADR находятся в `apps/docs/docs/adr/` и отображаются в этом Docusaurus-разделе. diff --git a/apps/docs/docs/architecture.md b/apps/docs/docs/architecture.md index 6e9fc06..66359e7 100644 --- a/apps/docs/docs/architecture.md +++ b/apps/docs/docs/architecture.md @@ -1,94 +1,101 @@ -# Architecture +# Архитектура -## System Architecture +## Архитектура системы ```mermaid -graph TD +flowchart LR Browser["Browser
(React SPA)"] - Backend["NestJS API
:3000"] - Cache["In-Memory Cache
(cache-manager)"] + API["NestJS API
:3000"] + Cache["In-memory cache
(cache-manager)"] MOEX["MOEX ISS API
iss.moex.com"] - DB[("SQLite Database
(Prisma)")] + DB[("SQLite
(Prisma)")] - Browser -->|"/api/v1/*"| Backend - Browser -->|"Cookie: refreshToken"| Backend - Browser -->|"Authorization: Bearer"| Backend - Backend -->|"getOrFetch()"| Cache - Backend -->|"GET /iss/*.json"| MOEX - Backend -->|"Prisma ORM"| DB - Cache -->|"data"| Backend - DB -->|"users"| Backend - MOEX -->|"raw data"| Backend - - subgraph Backend_Internal["Backend (NestJS)"] - MoexClient["MoexClientModule
p-queue + circuit breaker"] - Auth["AuthModule
JWT + bcrypt + Guards"] - Shares["SharesModule"] - Bonds["BondsModule"] - Candles["CandlesModule"] - Securities["SecuritiesModule"] - Health["HealthModule"] - CacheService["CacheService
(global)"] - Prisma["PrismaService
(global)"] - - Auth -->|"user CRUD"| Prisma - MoexClient -->|"fetches"| Shares - MoexClient -->|"fetches"| Bonds - MoexClient -->|"fetches"| Candles - MoexClient -->|"fetches"| Securities - Shares -->|"uses"| CacheService - Bonds -->|"uses"| CacheService - Candles -->|"uses"| CacheService - Securities -->|"uses"| CacheService - end + Browser -->|"/api/v1/*"| API + Browser -->|"Cookie: refreshToken"| API + Browser -->|"Authorization: Bearer"| API + API -->|"getOrFetch()"| Cache + API -->|"GET /iss/*.json"| MOEX + API -->|"Prisma ORM"| DB + Cache -->|"данные"| API + DB -->|"пользователи и портфели"| API + MOEX -->|"сырые данные"| API ``` -## Request Flow (Authenticated) +## Внутренние модули backend + +```mermaid +flowchart LR + Auth["AuthModule
JWT + bcrypt + guards"] + Shares["SharesModule"] + Bonds["BondsModule"] + Candles["CandlesModule"] + Securities["SecuritiesModule"] + Portfolio["PortfolioModule"] + Health["HealthModule"] + MoexClient["MoexClientModule
p-queue + circuit breaker"] + CacheService["CacheService
(global)"] + Prisma["PrismaService
(global)"] + + Auth -->|"CRUD пользователей"| Prisma + Portfolio -->|"портфели и позиции"| Prisma + MoexClient -->|"запрашивает"| Shares + MoexClient -->|"запрашивает"| Bonds + MoexClient -->|"запрашивает"| Candles + MoexClient -->|"запрашивает"| Securities + MoexClient -->|"обогащает"| Portfolio + Shares -->|"использует"| CacheService + Bonds -->|"использует"| CacheService + Candles -->|"использует"| CacheService + Securities -->|"использует"| CacheService + Portfolio -->|"использует"| CacheService +``` + +## Поток авторизованного запроса ```mermaid sequenceDiagram - participant User + participant User as Пользователь participant Frontend as React SPA participant Backend as NestJS API - participant Cache as In-Memory Cache + participant Cache as In-memory cache participant MOEX as MOEX ISS participant AuthDB as SQLite - User->>Frontend: View stock SBER - Note over Frontend: Access token in memory + User->>Frontend: Открывает акцию SBER + Note over Frontend: access token хранится в памяти Frontend->>Backend: GET /securities/shares/SBER (Authorization: Bearer) - Backend->>Backend: JwtAuthGuard validates token + Backend->>Backend: JwtAuthGuard проверяет token Backend->>Cache: getOrFetch('share:SBER') - alt Cache miss + alt cache miss Cache->>Backend: null Backend->>MOEX: GET /iss/.../SBER.json - MOEX-->>Backend: raw data + MOEX-->>Backend: сырые данные Backend->>Cache: set('share:SBER', ..., TTL=900) - else Cache hit + else cache hit Cache-->>Backend: cached data end Backend-->>Frontend: { data, meta } - Frontend-->>User: rendered UI + Frontend-->>User: отрисованный UI ``` -## Architecture Decisions +## Архитектурные решения -All architectural decisions are documented as ADR pages in this documentation app: +Все архитектурные решения описаны в ADR-страницах этой документации: -| ADR | Summary | +| ADR | Кратко | |---|---| -| [ADR-001](adr/ADR-001-backend-single-point-of-access) | Backend — single point of access to MOEX | -| [ADR-002](adr/ADR-002-in-memory-cache) | In-memory cache (cache-manager) | -| [ADR-003](adr/ADR-003-rate-limiting-strategy) | Rate limiting with p-queue | -| [ADR-004](adr/ADR-004-feature-modules) | Feature modules architecture | -| [ADR-005](adr/ADR-005-openapi-codegen-frontend) | OpenAPI codegen for frontend | -| [ADR-006](adr/ADR-006-no-cci) | No CCI (Custom Components Infrastructure) | -| [ADR-007](adr/ADR-007-two-level-caching) | Two-level caching strategy | -| [ADR-008](adr/ADR-008-auth-system) | Authentication & Authorization | -| [ADR-009](adr/ADR-009-portfolio-domain) | Portfolio domain model | -| [ADR-010](adr/ADR-010-backend-price-computation) | Backend price computation | +| [ADR-001](adr/ADR-001-backend-single-point-of-access) | Backend — единственная точка доступа к MOEX | +| [ADR-002](adr/ADR-002-in-memory-cache) | In-memory cache через cache-manager | +| [ADR-003](adr/ADR-003-rate-limiting-strategy) | Rate limiting через p-queue | +| [ADR-004](adr/ADR-004-feature-modules) | Архитектура feature-модулей | +| [ADR-005](adr/ADR-005-openapi-codegen-frontend) | OpenAPI codegen для frontend | +| [ADR-006](adr/ADR-006-no-cci) | Отказ от CCI (Custom Components Infrastructure) | +| [ADR-007](adr/ADR-007-two-level-caching) | Двухуровневая стратегия cache | +| [ADR-008](adr/ADR-008-auth-system) | Authentication и Authorization | +| [ADR-009](adr/ADR-009-portfolio-domain) | Доменная модель портфеля | +| [ADR-010](adr/ADR-010-backend-price-computation) | Расчёт цен на backend | -## Response Format +## Формат ответа Все ответы API обёрнуты в единый формат: @@ -114,9 +121,9 @@ All architectural decisions are documented as ADR pages in this documentation ap } ``` -## Global Configuration +## Глобальная конфигурация -- Global prefix: `/api/v1` +- Глобальный префикс: `/api/v1` - ValidationPipe: `transform: true, whitelist: true` - HttpExceptionFilter (catch-all) - TransformInterceptor (авто-обёртка в `ApiResponse`) diff --git a/apps/docs/docs/backend/api.md b/apps/docs/docs/backend/api.md index 11af40d..6945f10 100644 --- a/apps/docs/docs/backend/api.md +++ b/apps/docs/docs/backend/api.md @@ -1,4 +1,4 @@ -# API Reference +# API reference Все эндпоинты находятся под префиксом `/api/v1`. Swagger UI: `/api/docs`. @@ -104,13 +104,13 @@ Поиск по инструментам. -**Parameters:** +**Параметры:** -| Param | Type | Required | Default | Description | +| Параметр | Тип | Обязателен | По умолчанию | Описание | |---|---|---|---|---| -| `q` | string | yes | — | Поисковый запрос (1-100 символов) | -| `type` | enum | no | `all` | Фильтр: `all`, `share`, `bond` | -| `limit` | integer | no | `20` | Лимит результатов | +| `q` | string | да | — | Поисковый запрос (1-100 символов) | +| `type` | enum | нет | `all` | Фильтр: `all`, `share`, `bond` | +| `limit` | integer | нет | `20` | Лимит результатов | **Response:** ```json @@ -134,18 +134,18 @@ Скринер ценных бумаг по параметрам цены, объёма, доходности, дюрации, купона и срока погашения. -**Parameters:** +**Параметры:** -| Param | Type | Required | Description | +| Параметр | Тип | Обязателен | Описание | |---|---|---|---| -| `type` | enum | yes | `share` или `bond` | -| `priceMin`, `priceMax` | number | no | Диапазон цены | -| `volumeMin` | number | no | Минимальный объём | -| `yieldMin`, `yieldMax` | number | no | Диапазон доходности облигаций | -| `durationMin`, `durationMax` | number | no | Диапазон дюрации | -| `sortBy` | string | no | Поле сортировки | -| `sortOrder` | enum | no | `asc` или `desc` | -| `page`, `pageSize` | integer | no | Пагинация | +| `type` | enum | да | `share` или `bond` | +| `priceMin`, `priceMax` | number | нет | Диапазон цены | +| `volumeMin` | number | нет | Минимальный объём | +| `yieldMin`, `yieldMax` | number | нет | Диапазон доходности облигаций | +| `durationMin`, `durationMax` | number | нет | Диапазон дюрации | +| `sortBy` | string | нет | Поле сортировки | +| `sortOrder` | enum | нет | `asc` или `desc` | +| `page`, `pageSize` | integer | нет | Пагинация | **Response:** `{ data: { items, total, page, pageSize, totalPages }, meta }`. @@ -155,7 +155,7 @@ Спецификация акции. -**Parameters:** `secid` — тикер (например, `SBER`) +**Параметры:** `secid` — тикер (например, `SBER`) **Response:** `ShareResponse` — спецификация + текущие рыночные данные. @@ -216,7 +216,7 @@ Дивиденды акции. -**Parameters:** `secid` — тикер +**Параметры:** `secid` — тикер **Response:** ```json @@ -236,12 +236,12 @@ Дневная история торгов акции. -**Parameters:** +**Параметры:** -| Param | Type | Required | Description | +| Параметр | Тип | Обязателен | Описание | |---|---|---|---| -| `from` | string (date) | yes | Начальная дата (`YYYY-MM-DD`) | -| `till` | string (date) | yes | Конечная дата (`YYYY-MM-DD`) | +| `from` | string (date) | да | Начальная дата (`YYYY-MM-DD`) | +| `till` | string (date) | да | Конечная дата (`YYYY-MM-DD`) | **Response:** ```json @@ -267,7 +267,7 @@ Спецификация облигации. -**Parameters:** `secid` — тикер +**Параметры:** `secid` — тикер **Response:** `BondResponse` — спецификация + рыночные данные. @@ -320,7 +320,7 @@ Дневная история торгов облигации. -**Parameters:** `from`, `till` (date) +**Параметры:** `from`, `till` (date) **Response:** ```json @@ -347,13 +347,13 @@ Свечи облигации. -**Parameters:** +**Параметры:** -| Param | Type | Required | Description | +| Параметр | Тип | Обязателен | Описание | |---|---|---|---| -| `interval` | enum | yes | `1h` или `24h` | -| `from` | string (date) | yes | Начальная дата | -| `till` | string (date) | yes | Конечная дата | +| `interval` | enum | да | `1h` или `24h` | +| `from` | string (date) | да | Начальная дата | +| `till` | string (date) | да | Конечная дата | **Response:** ```json @@ -374,11 +374,11 @@ } ``` -## Portfolios +## Портфели Все portfolio endpoints защищены JWT и возвращают envelope `{ data, meta }`. -| Endpoint | Method | Description | +| Endpoint | Method | Описание | |---|---|---| | `/api/v1/portfolios` | GET | Список портфелей пользователя | | `/api/v1/portfolios` | POST | Создать портфель | diff --git a/apps/docs/docs/backend/auth.md b/apps/docs/docs/backend/auth.md index 22f653e..7c86c14 100644 --- a/apps/docs/docs/backend/auth.md +++ b/apps/docs/docs/backend/auth.md @@ -1,71 +1,71 @@ -# Authentication & Authorization +# Authentication и Authorization -## Architecture +## Архитектура -### Token-Based Authentication +### Token-based authentication Система использует два типа JWT-токенов: -| Token | Format | TTL | Storage | Purpose | +| Token | Формат | TTL | Хранение | Назначение | |-------|--------|-----|---------|---------| -| Access Token | JWT `{ sub, email, role }` | 15 min | Memory (React) + `Authorization: Bearer` | Authenticate API requests | -| Refresh Token | JWT `{ sub, jti }` | 7 days | httpOnly cookie + SHA-256 hash in DB | Issue new access tokens | +| Access token | JWT `{ sub, email, role }` | 15 мин | Память React + `Authorization: Bearer` | Аутентифицирует API requests | +| Refresh token | JWT `{ sub, jti }` | 7 дней | httpOnly cookie + SHA-256 hash в БД | Выпускает новые access tokens | -### Security +### Безопасность -- Passwords hashed with **bcrypt** (12 salt rounds) -- Refresh tokens stored as **bcrypt hash** in database -- httpOnly, SameSite=Lax, Secure (production) cookies -- Access token never persisted to localStorage (XSS protection) -- Auto-refresh on 401 with automatic retry of failed request -- Refresh token rotation: each refresh invalidates the old token +- Пароли хешируются через **bcrypt** (12 salt rounds) +- Refresh tokens хранятся в базе как **bcrypt hash** +- Cookie: httpOnly, SameSite=Lax, Secure в production +- Access token не сохраняется в localStorage, чтобы снизить XSS-риск +- При 401 frontend автоматически обновляет token и повторяет исходный request +- Refresh token rotation: каждый refresh инвалидирует старый token -### Flow +### Поток ```mermaid sequenceDiagram - participant User + participant User as Пользователь participant Frontend as React SPA participant Backend as NestJS API participant DB as SQLite (Prisma) - User->>Frontend: Enter email & password + User->>Frontend: Вводит email и password Frontend->>Backend: POST /auth/login { email, password } - Backend->>DB: Find user by email + Backend->>DB: Ищет пользователя по email Backend->>Backend: bcrypt.compare(password, hash) - Backend->>DB: Save refresh token hash + Backend->>DB: Сохраняет refresh token hash Backend-->>Frontend: { user, accessToken } + Set-Cookie (refreshToken) - Frontend->>Frontend: Store accessToken in memory - Frontend-->>User: Redirect to app + Frontend->>Frontend: Хранит accessToken в памяти + Frontend-->>User: Перенаправляет в приложение - Note over Frontend,Backend: Later API request + Note over Frontend,Backend: Следующий API request Frontend->>Backend: GET /auth/me (Authorization: Bearer ) - Backend->>Backend: Verify JWT signature & expiry + Backend->>Backend: Проверяет JWT signature и expiry Backend-->>Frontend: { user } - Note over Frontend,Backend: Token refresh (auto on 401) + Note over Frontend,Backend: Token refresh автоматически при 401 Frontend->>Backend: POST /auth/refresh (Cookie: refreshToken) - Backend->>Backend: Verify refresh JWT - Backend->>DB: Compare refresh token hash - Backend->>DB: Rotate: save new refresh token hash + Backend->>Backend: Проверяет refresh JWT + Backend->>DB: Сравнивает refresh token hash + Backend->>DB: Ротация: сохраняет новый refresh token hash Backend-->>Frontend: { user, newAccessToken } + Set-Cookie (newRefreshToken) - Frontend->>Frontend: Update accessToken in memory - Frontend->>Backend: Retry original request + Frontend->>Frontend: Обновляет accessToken в памяти + Frontend->>Backend: Повторяет исходный request Note over Frontend,Backend: Logout Frontend->>Backend: POST /auth/logout - Backend->>DB: Clear refresh token hash - Backend-->>Frontend: Clear cookie - Frontend->>Frontend: Clear accessToken + Backend->>DB: Очищает refresh token hash + Backend-->>Frontend: Очищает cookie + Frontend->>Frontend: Очищает accessToken ``` -## API Endpoints +## API endpoints -All endpoints are under `/api/v1/auth`. +Все endpoints находятся под `/api/v1/auth`. ### `POST /auth/register` -Register a new user. +Регистрирует нового пользователя. **Request:** ```json @@ -76,11 +76,11 @@ Register a new user. } ``` -**Response:** `{ data: { user, accessToken }, meta }` + `Set-Cookie` with refresh token. +**Response:** `{ data: { user, accessToken }, meta }` + `Set-Cookie` с refresh token. ### `POST /auth/login` -Authenticate existing user. +Аутентифицирует существующего пользователя. **Request:** ```json @@ -94,19 +94,19 @@ Authenticate existing user. ### `POST /auth/refresh` -Refresh access token. Reads refresh token from cookie. +Обновляет access token. Refresh token читается из cookie. **Response:** `{ data: { user, accessToken }, meta }` + new `Set-Cookie`. ### `POST /auth/logout` -Invalidates refresh token. Requires `Authorization: Bearer`. +Инвалидирует refresh token. Требует `Authorization: Bearer`. -**Response:** `{ data: { message }, meta }` + cookie cleared. +**Response:** `{ data: { message }, meta }` + очищенная cookie. ### `GET /auth/me` -Returns current user profile. Requires `Authorization: Bearer`. +Возвращает профиль текущего пользователя. Требует `Authorization: Bearer`. **Response:** ```json @@ -123,7 +123,7 @@ Returns current user profile. Requires `Authorization: Bearer`. ### `PATCH /auth/me` -Update current user profile. Requires `Authorization: Bearer`. +Обновляет профиль текущего пользователя. Требует `Authorization: Bearer`. **Request:** ```json @@ -134,19 +134,20 @@ Update current user profile. Requires `Authorization: Bearer`. ## Authorization (RBAC) -| Role | Permissions | +| Роль | Права | |------|-------------| -| `user` | View/edit own profile | -| `admin` | All user permissions | +| `user` | Просмотр и редактирование своего профиля | +| `admin` | Все права пользователя | -**Application level:** Global `JwtAuthGuard` protects all routes by default. Use `@Public()` decorator to bypass. `RolesGuard` checks required roles from `@Roles()` decorator. +**Уровень приложения:** глобальный `JwtAuthGuard` по умолчанию защищает все routes. +Декоратор `@Public()` открывает публичные endpoints. `RolesGuard` проверяет роли из `@Roles()`. -## Environment Variables +## Переменные окружения -| Variable | Default | Description | +| Переменная | По умолчанию | Описание | |----------|---------|-------------| -| `JWT_SECRET` | `dev-jwt-secret-...` | Secret for access token signing | -| `JWT_REFRESH_SECRET` | `dev-refresh-secret-...` | Secret for refresh token signing | +| `JWT_SECRET` | `dev-jwt-secret-...` | Secret для подписи access token | +| `JWT_REFRESH_SECRET` | `dev-refresh-secret-...` | Secret для подписи refresh token | | `JWT_ACCESS_EXPIRES` | `15m` | Access token TTL | | `JWT_REFRESH_EXPIRES` | `7d` | Refresh token TTL | -| `DATABASE_URL` | `file:./dev.db` | SQLite database URL (Prisma format) | +| `DATABASE_URL` | `file:./dev.db` | URL SQLite в формате Prisma | diff --git a/apps/docs/docs/backend/caching.md b/apps/docs/docs/backend/caching.md index c5dd918..db9337b 100644 --- a/apps/docs/docs/backend/caching.md +++ b/apps/docs/docs/backend/caching.md @@ -1,22 +1,22 @@ -# Caching +# Кеширование -## Cache Strategy +## Стратегия cache In-memory кеш через `@nestjs/cache-manager` (cache-manager v5). Поддерживается миграция на Redis (см. ADR-002). -## Cache Flow +## Поток cache ```mermaid flowchart LR - Request["Request Data"] + Request["Запрос данных"] CacheService["CacheService.getOrFetch()"] BuildKey["buildKey(prefix, keyParts)"] CacheGet["cacheManager.get(key)"] - Hit["Cache HIT → return data"] + Hit["Cache HIT → вернуть данные"] Miss["Cache MISS"] - FetchFn["FetchFn() → get from MOEX"] + FetchFn["fetchFn() → получить из MOEX"] CacheSet["cacheManager.set(key, data, ttl)"] - Return["Return { data, fromCache, cachedAt }"] + Return["Вернуть { data, fromCache, cachedAt }"] Request --> CacheService CacheService --> BuildKey @@ -48,7 +48,7 @@ getOrFetch( - При cache hit возвращает `{ data, fromCache: true, cachedAt: null }` - При cache miss вызывает `fetchFn`, сохраняет результат с TTL и возвращает `{ data, fromCache: false, cachedAt: '...' }` -## Cache Module Configuration +## Конфигурация CacheModule `apps/backend/src/modules/cache/cache.module.ts`: @@ -68,13 +68,13 @@ getOrFetch( export class CacheModule {} ``` -## Per-Data TTL +## TTL по типам данных -| Data Type | Config Key | Default TTL | Config Variable | +| Тип данных | Config key | TTL по умолчанию | Переменная | |---|---|---|---| -| Market Data | `marketDataTtl` | 900s (15 min) | `CACHE_MARKET_DATA_TTL` | -| History | `historyTtl` | 3600s (1h) | `CACHE_HISTORY_TTL` | -| Candles | `candlesTtl` | 3600s (1h) | `CACHE_CANDLES_TTL` | -| Security Spec | `securityTtl` | 86400s (24h) | `CACHE_SECURITY_TTL` | -| Search | `searchTtl` | 3600s (1h) | `CACHE_SEARCH_TTL` | -| Dividends | `dividendsTtl` | 86400s (24h) | `CACHE_DIVIDENDS_TTL` | +| Рыночные данные | `marketDataTtl` | 900s (15 мин) | `CACHE_MARKET_DATA_TTL` | +| История | `historyTtl` | 3600s (1 ч) | `CACHE_HISTORY_TTL` | +| Свечи | `candlesTtl` | 3600s (1 ч) | `CACHE_CANDLES_TTL` | +| Спецификация инструмента | `securityTtl` | 86400s (24 ч) | `CACHE_SECURITY_TTL` | +| Поиск | `searchTtl` | 3600s (1 ч) | `CACHE_SEARCH_TTL` | +| Дивиденды | `dividendsTtl` | 86400s (24 ч) | `CACHE_DIVIDENDS_TTL` | diff --git a/apps/docs/docs/backend/configuration.md b/apps/docs/docs/backend/configuration.md index a90c3bf..d5a517c 100644 --- a/apps/docs/docs/backend/configuration.md +++ b/apps/docs/docs/backend/configuration.md @@ -1,10 +1,10 @@ -# Configuration +# Конфигурация Конфигурация загружается через `@nestjs/config` из `config/configuration.ts` и env-переменных. -## Environment Variables +## Переменные окружения -| Variable | Default | Description | +| Переменная | По умолчанию | Описание | |---|---|---| | `PORT` | `3000` | Порт HTTP-сервера | | `MOEX_BASE_URL` | `https://iss.moex.com/iss` | Базовый URL MOEX ISS API | @@ -18,7 +18,7 @@ | `CACHE_SEARCH_TTL` | `3600` | TTL результатов поиска (секунды) | | `CACHE_DIVIDENDS_TTL` | `86400` | TTL дивидендов (секунды) | -## Configuration File +## Файл конфигурации `apps/backend/src/config/configuration.ts`: diff --git a/apps/docs/docs/backend/database.md b/apps/docs/docs/backend/database.md index 2168651..e60b9c6 100644 --- a/apps/docs/docs/backend/database.md +++ b/apps/docs/docs/backend/database.md @@ -1,63 +1,128 @@ -# Database Schema +# Схема базы данных -## Overview +## Обзор Используется **SQLite** через **Prisma ORM 7**. База данных находится в `apps/backend/dev.db`. -## Schema +## Схема ```prisma generator client { - provider = "prisma-client" - output = "../src/generated/prisma" + provider = "prisma-client-js" } datasource db { provider = "sqlite" } +model Portfolio { + id Int @id @default(autoincrement()) + userId Int + name String + description String? + currency String @default("RUB") + targets String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + positions Position[] + + @@unique([userId, name]) +} + +model Position { + id Int @id @default(autoincrement()) + portfolioId Int + secid String + type String @default("share") + quantity Int + buyPrice Float? + buyDate DateTime? + notes String? + tags String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + portfolio Portfolio @relation(fields: [portfolioId], references: [id], onDelete: Cascade) + + @@unique([portfolioId, secid]) +} + model User { - id Int @id @default(autoincrement()) - email String @unique + id Int @id @default(autoincrement()) + email String @unique password String name String? - role String @default("user") + role String @default("user") refreshToken String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + portfolios Portfolio[] } ``` -## Tables +## Таблицы -### Users +### User -| Column | Type | Constraints | Description | +| Колонка | Тип | Ограничения | Описание | |--------|------|-------------|-------------| -| id | INTEGER | PK, AUTOINCREMENT | User ID | -| email | TEXT | UNIQUE, NOT NULL | Email address | -| password | TEXT | NOT NULL | bcrypt hash of password | -| name | TEXT | NULLABLE | Display name | -| role | TEXT | NOT NULL, DEFAULT 'user' | Role for RBAC | -| refreshToken | TEXT | NULLABLE | bcrypt hash of current refresh token | -| createdAt | DATETIME | NOT NULL | Timestamp | -| updatedAt | DATETIME | NOT NULL | Auto-updated timestamp | +| id | INTEGER | PK, AUTOINCREMENT | ID пользователя | +| email | TEXT | UNIQUE, NOT NULL | Email | +| password | TEXT | NOT NULL | bcrypt hash пароля | +| name | TEXT | NULLABLE | Отображаемое имя | +| role | TEXT | NOT NULL, DEFAULT 'user' | Роль для RBAC | +| refreshToken | TEXT | NULLABLE | bcrypt hash текущего refresh token | +| createdAt | DATETIME | NOT NULL | Дата создания | +| updatedAt | DATETIME | NOT NULL | Дата последнего обновления | + +### Portfolio + +| Колонка | Тип | Ограничения | Описание | +|--------|------|-------------|-------------| +| id | INTEGER | PK, AUTOINCREMENT | ID портфеля | +| userId | INTEGER | FK -> User, NOT NULL | Владелец портфеля | +| name | TEXT | NOT NULL, UNIQUE per user | Название портфеля | +| description | TEXT | NULLABLE | Описание | +| currency | TEXT | DEFAULT 'RUB' | Валюта портфеля | +| targets | TEXT | NULLABLE | Целевые доли или настройки в JSON-строке | +| createdAt | DATETIME | NOT NULL | Дата создания | +| updatedAt | DATETIME | NOT NULL | Дата последнего обновления | + +### Position + +| Колонка | Тип | Ограничения | Описание | +|--------|------|-------------|-------------| +| id | INTEGER | PK, AUTOINCREMENT | ID позиции | +| portfolioId | INTEGER | FK -> Portfolio, NOT NULL | Портфель | +| secid | TEXT | NOT NULL | MOEX security ID | +| type | TEXT | DEFAULT 'share' | `share` или `bond` | +| quantity | INTEGER | NOT NULL | Количество | +| buyPrice | REAL | NULLABLE | Цена покупки | +| buyDate | DATETIME | NULLABLE | Дата покупки | +| notes | TEXT | NULLABLE | Заметки | +| tags | TEXT | NULLABLE | Tags в JSON-строке | +| createdAt | DATETIME | NOT NULL | Дата создания | +| updatedAt | DATETIME | NOT NULL | Дата последнего обновления | ## Prisma Client -The Prisma client is generated to `apps/backend/src/generated/prisma/` and imported via `@/generated/prisma/client`. +Prisma client импортируется из `@prisma/client`. -Key service: `PrismaService` (`src/modules/prisma/prisma.service.ts`) — extends `PrismaClient`, handles connection lifecycle (`onModuleInit`/`onModuleDestroy`). Registered as a global module. +Ключевой сервис: `PrismaService` (`src/modules/prisma/prisma.service.ts`) расширяет +`PrismaClient`, управляет жизненным циклом подключения (`onModuleInit`/`onModuleDestroy`) и +регистрируется через глобальный модуль. -## Migrations +## Миграции -Migrations are stored in `apps/backend/prisma/migrations/`. To create a new migration: +Миграции лежат в `apps/backend/prisma/migrations/`. Создать новую миграцию: ```bash npx prisma migrate dev --name ``` -To apply migrations in production: +Применить миграции в production: ```bash npx prisma migrate deploy diff --git a/apps/docs/docs/backend/modules.md b/apps/docs/docs/backend/modules.md index 0b65711..4a76b31 100644 --- a/apps/docs/docs/backend/modules.md +++ b/apps/docs/docs/backend/modules.md @@ -1,55 +1,58 @@ -# Backend Modules +# Модули backend -## Module Dependency Graph +## Граф зависимостей модулей + +### Импорты `AppModule` ```mermaid -graph TD - AppModule --> ConfigModule - AppModule --> CacheModule - AppModule --> MoexClientModule - AppModule --> PrismaModule - AppModule --> HealthModule - AppModule --> AuthModule - AppModule --> SecuritiesModule - AppModule --> SharesModule - AppModule --> BondsModule - AppModule --> CandlesModule - AppModule --> PortfolioModule +flowchart TB + AppModule["AppModule"] + GlobalModules["Глобальные модули
ConfigModule
PrismaModule
CacheModule
MoexClientModule"] + FeatureModules["Feature-модули
HealthModule
AuthModule
PortfolioModule
SecuritiesModule
SharesModule
BondsModule
CandlesModule"] - subgraph Global_Modules["Global Modules"] - PrismaModule - CacheModule - MoexClientModule - end - - AuthModule --> PrismaService["PrismaService"] - PortfolioModule --> PrismaService - PortfolioModule --> MoexClientService - SharesModule --> CacheService["CacheService"] - BondsModule --> CacheService - SecuritiesModule --> CacheService - CandlesModule --> CacheService - - SharesModule --> MoexClientService["MoexClientService"] - BondsModule --> MoexClientService - SecuritiesModule --> MoexClientService - CandlesModule --> MoexClientService + AppModule --> GlobalModules + AppModule --> FeatureModules ``` -## Module List +### Зависимости от сервисов -| Module | Global | Path | Description | +```mermaid +flowchart TB + AuthModule["AuthModule"] + PortfolioModule["PortfolioModule"] + MarketModules["SecuritiesModule
SharesModule
BondsModule
CandlesModule"] + + PrismaService["PrismaService"] + CacheService["CacheService"] + MoexClientService["MoexClientService"] + + PrismaModule --> PrismaService + CacheModule --> CacheService + MoexClientModule --> MoexClientService + + AuthModule --> PrismaService + PortfolioModule --> PrismaService + PortfolioModule --> CacheService + PortfolioModule --> MoexClientService + + MarketModules --> CacheService + MarketModules --> MoexClientService +``` + +## Список модулей + +| Модуль | Глобальный | Путь | Описание | |---|---|---|---| -| `PrismaModule` | Yes | `modules/prisma/` | Prisma client for SQLite | -| `CacheModule` | Yes | `modules/cache/` | In-memory cache (cache-manager) | -| `MoexClientModule` | Yes | `modules/moex-client/` | HTTP-клиент MOEX ISS | -| `HealthModule` | No | `modules/health/` | Health check endpoint | -| `AuthModule` | No | `modules/auth/` | JWT auth, refresh cookie, guards | -| `SecuritiesModule` | No | `modules/securities/` | Поиск инструментов | -| `SharesModule` | No | `modules/shares/` | Акции | -| `BondsModule` | No | `modules/bonds/` | Облигации | -| `CandlesModule` | No | `modules/candles/` | Свечи OHLCV | -| `PortfolioModule` | No | `modules/portfolio/` | Пользовательские портфели и аналитика | +| `PrismaModule` | Да | `modules/prisma/` | Prisma client для SQLite | +| `CacheModule` | Да | `modules/cache/` | In-memory cache через cache-manager | +| `MoexClientModule` | Да | `modules/moex-client/` | HTTP-клиент MOEX ISS | +| `HealthModule` | Нет | `modules/health/` | Health check endpoint | +| `AuthModule` | Нет | `modules/auth/` | JWT auth, refresh cookie, guards | +| `SecuritiesModule` | Нет | `modules/securities/` | Поиск инструментов | +| `SharesModule` | Нет | `modules/shares/` | Акции | +| `BondsModule` | Нет | `modules/bonds/` | Облигации | +| `CandlesModule` | Нет | `modules/candles/` | Свечи OHLCV | +| `PortfolioModule` | Нет | `modules/portfolio/` | Пользовательские портфели и аналитика | ### PrismaModule diff --git a/apps/docs/docs/backend/moex-client.md b/apps/docs/docs/backend/moex-client.md index b2dadf6..b55e8b9 100644 --- a/apps/docs/docs/backend/moex-client.md +++ b/apps/docs/docs/backend/moex-client.md @@ -1,10 +1,10 @@ # MOEX Client -## Overview +## Обзор `MoexClientService` (`apps/backend/src/modules/moex-client/moex-client.service.ts`) — HTTP-клиент для MOEX ISS API. -## Rate Limiting +## Rate limiting Использует `p-queue`: @@ -17,7 +17,7 @@ this.queue = new PQueue({ Все запросы к MOEX проходят через очередь — не более `MOEX_RATE_LIMIT` запросов в секунду. -## Circuit Breaker +## Circuit breaker Состояние: закрыт → открыт → полуоткрыт (через таймаут). @@ -30,7 +30,7 @@ private circuitErrorCount = 0; - В открытом состоянии все запросы мгновенно падают с ошибкой `"Circuit breaker is open"` - Через `MOEX_CIRCUIT_BREAKER_RESET_SECONDS` (30) автоматически сбрасывается -## Request Method +## Метод request ```typescript private async request(path: string, params?: Record): Promise @@ -40,7 +40,7 @@ private async request(path: string, params?: Record): Promise - Устанавливает `iss.meta=off` (отключает метаданные) - Таймаут: 10s -## Response Parsing +## Разбор response MOEX возвращает данные в табличном формате: @@ -55,9 +55,9 @@ MOEX возвращает данные в табличном формате: Метод `extractTable` преобразует это в массив объектов по колонкам. -## Available MOEX Methods +## Доступные методы MOEX -| Method | MOEX Path | Description | +| Метод | MOEX path | Описание | |---|---|---| | `searchSecurities` | `/securities?q=` | Поиск инструментов | | `getSecurityDescription` | `/securities/{secid}` | Спецификация | @@ -69,7 +69,7 @@ MOEX возвращает данные в табличном формате: | `getHistory` | `/engines/stock/markets/shares/securities/{secid}` | История акций | | `getBondHistory` | `/engines/stock/markets/bonds/securities/{secid}` | История облигаций | -## MOEX ISS Types +## MOEX ISS types Все MOEX-типы описаны в `apps/backend/src/modules/moex-client/moex-client.types.ts`: diff --git a/apps/docs/docs/backend/overview.md b/apps/docs/docs/backend/overview.md index 3b918e4..2702eec 100644 --- a/apps/docs/docs/backend/overview.md +++ b/apps/docs/docs/backend/overview.md @@ -1,8 +1,8 @@ -# Backend Overview +# Обзор backend Backend — NestJS-приложение, единственная точка доступа к MOEX ISS. -## Entry Point +## Точка входа `apps/backend/src/main.ts` — bootstrap: @@ -12,16 +12,16 @@ Backend — NestJS-приложение, единственная точка д - JwtAuthGuard (глобально, `@Public()` для открытых эндпоинтов) - cookie-parser (для refresh token) - CORS включён (`credentials: true`) -- Порт из `PORT` env (default: 3000) +- Порт из `PORT` env (по умолчанию: 3000) -## Root Module +## Корневой модуль `apps/backend/src/app.module.ts` импортирует: - `ConfigModule.forRoot` (глобальный, из `config/configuration.ts`) - Импортируются 8 feature-модулей (Prisma + Cache + MoexClient — глобальные) -## Source Layout +## Структура исходников ``` apps/backend/src/ diff --git a/apps/docs/docs/backend/portfolio.md b/apps/docs/docs/backend/portfolio.md index 92914f4..8953965 100644 --- a/apps/docs/docs/backend/portfolio.md +++ b/apps/docs/docs/backend/portfolio.md @@ -1,19 +1,19 @@ -# Portfolio Module +# PortfolioModule -Portfolio module позволяет пользователям создавать и вести виртуальные инвестиционные портфели для +`PortfolioModule` позволяет пользователям создавать и вести виртуальные инвестиционные портфели для аналитики и отслеживания позиций. -## Overview +## Обзор - **Backend:** `PortfolioModule` (`apps/backend/src/modules/portfolio/`) -- **Frontend:** Protected pages at `/portfolios` and `/portfolios/:id` -- **Database:** `Portfolio` and `Position` models (Prisma + SQLite) +- **Frontend:** защищённые страницы `/portfolios` и `/portfolios/:id` +- **База данных:** модели `Portfolio` и `Position` (Prisma + SQLite) -## API Endpoints +## API endpoints -Все endpoints требуют JWT authentication (`JwtAuthGuard`). +Все endpoints требуют JWT authentication через `JwtAuthGuard`. -| Endpoint | Method | Description | +| Endpoint | Method | Описание | |---|---|---| | `/api/v1/portfolios` | GET | Список портфелей пользователя | | `/api/v1/portfolios` | POST | Создать портфель | @@ -25,7 +25,7 @@ Portfolio module позволяет пользователям создават | `/api/v1/portfolios/:id/positions/:positionId` | PATCH | Обновить позицию | | `/api/v1/portfolios/:id/positions/:positionId` | DELETE | Удалить позицию | -## Domain Model +## Доменная модель ``` Portfolio @@ -50,56 +50,63 @@ Position couponPeriod, bondType, offerDate ``` -## Analytics and PnL +## Analytics и PnL -The backend calculates performance metrics for each position and the portfolio as a whole: +Backend рассчитывает метрики доходности для каждой позиции и портфеля целиком: -- **Total Cost:** `buyPrice * quantity` (adjusted for bonds: `(buyPrice / 100) * faceValue * quantity`). +- **Total cost:** `buyPrice * quantity`; для облигаций: `(buyPrice / 100) * faceValue * quantity`. - **Unrealized PnL:** `currentValue - totalCost`. - **PnL %:** `(PnL / totalCost) * 100`. -- **Weighted Yield:** Portfolio-wide average yield based on position weights. +- **Weighted yield:** средняя доходность портфеля с учётом весов позиций. -Analytics are available via the `/analytics` endpoint or as a `summary` object in the `/portfolios/:id` detail response. +Analytics доступны через endpoint `/analytics` или как объект `summary` в детальном response +`/portfolios/:id`. -## Security Type Detection +## Определение типа инструмента -When adding a position, the backend fetches `getSecurityDescription(secid)` from MOEX. If `desc.group === 'stock_bonds'`, the position type is set to `bond`; otherwise `share`. This determines which enrichment path is used on read. +При добавлении позиции backend получает из MOEX `getSecurityDescription(secid)`. Если +`desc.group === 'stock_bonds'`, тип позиции становится `bond`; иначе используется `share`. От типа +зависит путь обогащения при чтении портфеля. -## Price Computation (Shares) +## Расчёт цены акций -For shares: `currentPrice = marketData.last`, `currentValue = price * quantity`, with `change` and `changePercent` from `lastChange` and `lastChangePrcnt`. +Для акций: `currentPrice = marketData.last`, `currentValue = price * quantity`; `change` и +`changePercent` берутся из `lastChange` и `lastChangePrcnt`. -## Price Computation (Bonds) +## Расчёт цены облигаций -For bonds, MOEX returns prices as a percentage of face value (e.g., 98.5 = 98.5% of 1000 RUB). +Для облигаций MOEX возвращает цены в процентах от номинала: например, 98.5 означает 98.5% от +1000 RUB. - `currentPrice = marketData.last` (% of face value) - `currentValue = (price / 100) * faceValue * quantity` -Bond enrichment also fetches: -| Field | Source | Description | +Обогащение облигаций также получает: +| Поле | Источник | Описание | |---|---|---| | `yieldToMaturity` | `getBondMarketData().yield` | YTM (%) | -| `duration` | `getBondMarketData().duration` | Modified duration (years) | -| `couponValue` | `getBondData().couponValue` | Coupon amount in RUB | -| `couponPercent` | `getBondData().couponPercent` | Coupon rate (%) | -| `couponPeriod` | `getBondData().couponPeriod` | Days between payments | -| `nextCouponDate` | `getBondData().nextCoupon` | Next coupon date | -| `matDate` | `getBondData().matDate` | Maturity date | -| `offerDate` | `getBondData().offerDate` | Early redemption date | -| `accruedInt` | `getBondData().accruedInt` | Accrued interest per bond (RUB) | +| `duration` | `getBondMarketData().duration` | Modified duration в годах | +| `couponValue` | `getBondData().couponValue` | Размер купона в RUB | +| `couponPercent` | `getBondData().couponPercent` | Ставка купона (%) | +| `couponPeriod` | `getBondData().couponPeriod` | Дней между выплатами | +| `nextCouponDate` | `getBondData().nextCoupon` | Дата следующего купона | +| `matDate` | `getBondData().matDate` | Дата погашения | +| `offerDate` | `getBondData().offerDate` | Дата досрочного погашения | +| `accruedInt` | `getBondData().accruedInt` | НКД на облигацию (RUB) | | `bondType` | `getBondData().bondType` | "ОФЗ", "Корпоративная", etc. | -| `bid` / `offer` | `getBondMarketData()` | Current bid/ask (% of face) | +| `bid` / `offer` | `getBondMarketData()` | Текущий bid/ask в % от номинала | -## MOEX Board Selection +## Выбор MOEX board -The backend uses the following default MOEX boards: -- **Shares:** `TQBR` (Т+: Акции — безадрес.) -- **Bonds:** `TQCB` (Т+: Корпоративные облигации — безадрес.) +Backend использует следующие MOEX boards по умолчанию: +- **Акции:** `TQBR` (Т+: Акции — безадрес.) +- **Облигации:** `TQCB` (Т+: Корпоративные облигации — безадрес.) -Some bonds (primarily OFZ government bonds) trade on `TQOB` board. If the requested board has no trading data, the backend falls back to any board with a non-null `LAST` price. Data is cached with standard market data TTL (900s). +Некоторые облигации, в первую очередь ОФЗ, торгуются на board `TQOB`. Если на запрошенном board нет +торговых данных, backend выбирает любой board с ненулевой ценой `LAST`. Данные кешируются со +стандартным market data TTL (900s). -## Future Phases +## Будущие этапы 1. **Positions v2** — pie chart, filtering 2. **Transactions** — buy/sell history, average cost basis diff --git a/apps/docs/docs/backend/securities.md b/apps/docs/docs/backend/securities.md index 7e766f3..7a52d40 100644 --- a/apps/docs/docs/backend/securities.md +++ b/apps/docs/docs/backend/securities.md @@ -1,41 +1,43 @@ -# Securities Module +# SecuritiesModule -The Securities module provides search and filtering capabilities for MOEX financial instruments. +`SecuritiesModule` отвечает за поиск и фильтрацию финансовых инструментов MOEX. -## Overview +## Обзор - **Backend:** `SecuritiesModule` (`apps/backend/src/modules/securities/`) -- **Frontend:** Search bar in layout, dedicated Screener page at `/screener` -- **Source:** MOEX ISS API +- **Frontend:** поисковая строка в layout и отдельная страница Screener по `/screener` +- **Источник:** MOEX ISS API ## Search API `GET /api/v1/securities/search?q={query}&type={type}&limit={limit}` -Searches for securities by ticker or name. -- **type:** `all` (default), `share`, `bond`. -- **Results:** List of `SearchResultItem` with `secid`, `isin`, `shortName`, `type`, `listLevel`. +Ищет ценные бумаги по тикеру или названию. +- **type:** `all` по умолчанию, `share`, `bond`. +- **Результат:** список `SearchResultItem` с `secid`, `isin`, `shortName`, `type`, `listLevel`. ## Security Screener `GET /api/v1/securities/screener` -Allows filtering the entire MOEX board (shares or bonds) by market parameters. +Фильтрует весь board MOEX для акций или облигаций по рыночным параметрам. -### Query Parameters +### Query parameters -| Parameter | Type | Description | +| Параметр | Тип | Описание | |---|---|---| -| `type` | `share` \| `bond` | **Required.** Board type to scan. | -| `priceMin` / `Max` | number | Price range (RUB for shares, % for bonds) | -| `volumeMin` | number | Minimum daily trading volume | -| `yieldMin` / `Max` | number | YTM range (Bonds only) | -| `durationMin` / `Max`| number | Duration range in years (Bonds only) | -| `changePercentMin` | number | Minimum daily price change % (Shares only) | -| `sortBy` | string | Field to sort by (`price`, `volume`, `yieldToMaturity`, etc.) | -| `sortOrder` | `asc` \| `desc` | Sort direction | -| `page` / `pageSize` | number | Pagination parameters | +| `type` | `share` \| `bond` | **Обязателен.** Тип board для сканирования. | +| `priceMin` / `Max` | number | Диапазон цены: RUB для акций, проценты для облигаций | +| `volumeMin` | number | Минимальный дневной объём торгов | +| `yieldMin` / `Max` | number | Диапазон YTM, только для облигаций | +| `durationMin` / `Max`| number | Диапазон duration в годах, только для облигаций | +| `changePercentMin` | number | Минимальное дневное изменение цены в %, только для акций | +| `sortBy` | string | Поле сортировки: `price`, `volume`, `yieldToMaturity`, etc. | +| `sortOrder` | `asc` \| `desc` | Направление сортировки | +| `page` / `pageSize` | number | Параметры пагинации | -### Implementation Details +### Детали реализации -The Screener fetches the full market board from MOEX in a single batch request, caches it for 60 seconds (standard market data TTL), and applies filtering/sorting/pagination on the backend. This ensures high performance for complex queries without overwhelming the MOEX API. +Screener получает полный market board из MOEX одним batch request, кеширует данные на 60 секунд +или стандартный market data TTL и применяет фильтрацию, сортировку и пагинацию на backend. Так +сложные запросы остаются быстрыми и не перегружают MOEX API. diff --git a/apps/docs/docs/development/codegen.md b/apps/docs/docs/development/codegen.md index 5bd116c..66fe1e4 100644 --- a/apps/docs/docs/development/codegen.md +++ b/apps/docs/docs/development/codegen.md @@ -1,10 +1,10 @@ -# Code Generation +# Генерация кода -## OpenAPI Types +## OpenAPI types Frontend генерирует TypeScript-типы из Swagger-спецификации бэкенда. -### Generate Types +### Сгенерировать типы ```bash npm run codegen -w apps/frontend @@ -16,17 +16,17 @@ npm run codegen -w apps/frontend openapi-typescript http://localhost:3000/api/docs-json -o src/api/types.ts ``` -### Prerequisites +### Требования - Backend должен быть запущен на `localhost:3000` - Swagger UI доступен на `http://localhost:3000/api/docs` -### Output +### Результат - `apps/frontend/src/api/types.ts` — сгенерированные типы `paths` и `operations` - Live Swagger JSON на `http://localhost:3000/api/docs-json` остаётся источником OpenAPI-контракта -### Verify Artifacts +### Проверка артефактов После регенерации OpenAPI artifacts запустите: @@ -37,6 +37,6 @@ npm run test -w apps/backend -- src/openapi-artifacts.spec.ts Тест проверяет, что checked-in frontend types содержат актуальные auth, screener и portfolio paths и не содержат локальный alternate port. -### Manual Types +### Ручные типы Помимо codegen, используются рукописные типы в `apps/frontend/src/api/responses.ts`. Они не полностью соответствуют codegen'овым и поддерживаются вручную. diff --git a/apps/docs/docs/development/commands.md b/apps/docs/docs/development/commands.md index aee5e8f..1d66501 100644 --- a/apps/docs/docs/development/commands.md +++ b/apps/docs/docs/development/commands.md @@ -1,8 +1,8 @@ -# Commands +# Команды -## Root Workspace +## Корневой workspace -| Command | Description | +| Команда | Описание | |---|---| | `npm run dev:backend` | Запуск NestJS в режиме watch на :3000 | | `npm run dev:frontend` | Vite dev-сервер на `:5173`, проксирует `/api` на backend | @@ -16,9 +16,9 @@ | `npm run format` | Prettier для всех `*.{ts,tsx}` | | `npm run format:check` | Проверка Prettier для всех `*.{ts,tsx}` | -## Backend Workspace +## Backend workspace -| Command | Description | +| Команда | Описание | |---|---| | `npm run build -w apps/backend` | `nest build` | | `npm run start:dev -w apps/backend` | `nest start --watch` | @@ -28,9 +28,9 @@ | `npm run test:watch -w apps/backend` | `vitest` | | `npm run test:integration -w apps/backend` | Opt-in live MOEX integration tests, требуется network access | -## Frontend Workspace +## Frontend workspace -| Command | Description | +| Команда | Описание | |---|---| | `npm run dev -w apps/frontend` | `vite` | | `npm run build -w apps/frontend` | `tsc -b && vite build` | @@ -39,15 +39,15 @@ | `npm run lint -w apps/frontend` | ESLint для `src/**/*.{ts,tsx}` | | `npm run test -w apps/frontend` | Frontend Vitest suite | -## Docs Workspace +## Docs workspace -| Command | Description | +| Команда | Описание | |---|---| | `npm run dev -w apps/docs` | Docusaurus dev-server | | `npm run build -w apps/docs` | Production build документации | | `npm run serve -w apps/docs` | Локальная проверка production build | -## Single Test +## Один тест ```bash npx vitest run apps/backend/src/modules/shares/shares.service.spec.ts -w apps/backend diff --git a/apps/docs/docs/development/conventions.md b/apps/docs/docs/development/conventions.md index 63b5426..be82837 100644 --- a/apps/docs/docs/development/conventions.md +++ b/apps/docs/docs/development/conventions.md @@ -1,6 +1,6 @@ -# Code Conventions +# Соглашения по коду -## Formatting +## Форматирование Prettier (`.prettierrc`): @@ -26,17 +26,17 @@ ESLint запускается для backend (`apps/backend`) и frontend (`apps Запуск: `npm run lint` -## Naming (backend) +## Именование в backend - ПаскальКейс для модулей, контроллеров, сервисов - `const` для всех переменных (кроме случаев, где нужна мутация) - DTO-файлы в `dto/` внутри каждого модуля -## Imports (backend) +## Imports в backend Алиас: `@/*` → `apps/backend/src/*` -## Imports (frontend) +## Imports в frontend Алиас: `@/*` → `apps/frontend/src/*` @@ -50,7 +50,7 @@ ESLint запускается для backend (`apps/backend`) и frontend (`apps - Strict: true - ESModule interop: true -## Backend-specific +## Специфика backend - Использует SWC через `unplugin-swc` (vitest config) - Контроллеры содержат только маршрутизацию и вызовы сервисов diff --git a/apps/docs/docs/development/testing.md b/apps/docs/docs/development/testing.md index 85bcf1f..0b5e1d9 100644 --- a/apps/docs/docs/development/testing.md +++ b/apps/docs/docs/development/testing.md @@ -1,12 +1,12 @@ -# Testing +# Тестирование -## Backend Tests +## Тесты backend Фреймворк: **vitest** (с `unplugin-swc` для быстрой компиляции, не ts-jest). Конфигурация vitest в `apps/backend/` использует SWC для трансформации TypeScript. -### Run All Tests +### Запустить все тесты ```bash npm run test:backend @@ -14,21 +14,21 @@ npm run test:backend npm run test -w apps/backend ``` -### Run Single Test +### Запустить один тест ```bash npx vitest run apps/backend/src/modules/shares/shares.service.spec.ts -w apps/backend ``` -### Watch Mode +### Watch mode ```bash npm run test:watch -w apps/backend ``` -## Test Files +## Тестовые файлы -| File | What it tests | +| Файл | Что проверяет | |---|---| | `shares.service.spec.ts` | SharesService | | `bonds.service.spec.ts` | BondsService | @@ -37,7 +37,7 @@ npm run test:watch -w apps/backend | `securities.service.spec.ts` | SecuritiesService | | `moex-client.service.spec.ts` | MoexClientService | -## Frontend Tests +## Тесты frontend Фреймворк: **Vitest 4** + **React Testing Library** + **MSW**. @@ -51,7 +51,7 @@ npm run test -w apps/frontend Тесты покрывают API-клиент, auth context, hooks, базовые pages и shared components. -## Live MOEX Integration Tests +## Live MOEX integration tests Live MOEX checks вынесены из default backend suite. diff --git a/apps/docs/docs/frontend/api-client.md b/apps/docs/docs/frontend/api-client.md index f974de8..e6e916a 100644 --- a/apps/docs/docs/frontend/api-client.md +++ b/apps/docs/docs/frontend/api-client.md @@ -1,6 +1,6 @@ -# API Client +# API client -## Client (`api/client.ts`) +## Клиент (`api/client.ts`) Использует нативный `fetch` с единой обёрткой `request`. @@ -16,7 +16,7 @@ async function request( - Парсит JSON-ответ в `ApiEnvelope<{ data: T, meta: ApiResponseMeta }>` - Выбрасывает `Error` при HTTP-ошибке -## API Functions +## API functions | Function | Method | Path | |---|---|---| @@ -48,7 +48,7 @@ async function request( | `removePosition(portfolioId, positionId)` | DELETE | `/api/v1/portfolios/:id/positions/:positionId` | | `getPortfolioAnalytics(portfolioId)` | GET | `/api/v1/portfolios/:id/analytics` | -## Types +## Типы Ручные типы ответов в `api/responses.ts`: diff --git a/apps/docs/docs/frontend/components.md b/apps/docs/docs/frontend/components.md index f581e1e..4b5b3e5 100644 --- a/apps/docs/docs/frontend/components.md +++ b/apps/docs/docs/frontend/components.md @@ -1,4 +1,4 @@ -# Components +# Компоненты ## Layout (`components/Layout.tsx`) diff --git a/apps/docs/docs/frontend/hooks.md b/apps/docs/docs/frontend/hooks.md index 5b67ac6..79376cc 100644 --- a/apps/docs/docs/frontend/hooks.md +++ b/apps/docs/docs/frontend/hooks.md @@ -1,8 +1,8 @@ -# Hooks +# Хуки Все хуки используют TanStack Query v5. -| Hook | Query Key | Stale Time | Description | +| Хук | Query key | Stale time | Описание | |---|---|---|---| | `useSearch(query)` | `['securities', 'search', query]` | 60s | Поиск инструментов (enabled: query ≥ 2 символов) | | `useStock(secid)` | `['stock', secid]` | 900s | Спецификация акции | @@ -11,7 +11,7 @@ | `useBond(secid)` | `['bond', secid]` | 900s | Спецификация облигации | | `useBondCandles(secid, interval, from, till)` | `['bondCandles', secid, interval, from, till]` | 3600s | Свечи облигации | -## Query Configuration +## Конфигурация Query ```typescript const queryClient = new QueryClient({ @@ -25,7 +25,7 @@ const queryClient = new QueryClient({ }); ``` -## Hook Pattern +## Паттерн hook Каждый хук: diff --git a/apps/docs/docs/frontend/overview.md b/apps/docs/docs/frontend/overview.md index 3cb6e60..38010da 100644 --- a/apps/docs/docs/frontend/overview.md +++ b/apps/docs/docs/frontend/overview.md @@ -1,8 +1,8 @@ -# Frontend Overview +# Обзор frontend React SPA, собранная с Vite. -## Tech Stack +## Технологический стек - React 18 - react-router-dom v6 @@ -12,11 +12,11 @@ React SPA, собранная с Vite. - Vitest + Testing Library + MSW - Vite 5 -## Source Layout +## Структура исходников ``` apps/frontend/src/ -├── main.tsx # Entry point +├── main.tsx # Точка входа ├── App.tsx # BrowserRouter ├── routes.tsx # Маршруты ├── api/ @@ -65,6 +65,6 @@ apps/frontend/src/ └── styles.css ``` -## Entry Point +## Точка входа `main.tsx` рендерит `App.tsx`, который содержит `BrowserRouter` → `AppRoutes`. diff --git a/apps/docs/docs/frontend/routes.md b/apps/docs/docs/frontend/routes.md index 8fa479d..350f8cc 100644 --- a/apps/docs/docs/frontend/routes.md +++ b/apps/docs/docs/frontend/routes.md @@ -1,8 +1,8 @@ -# Routes +# Маршруты Определены в `apps/frontend/src/routes.tsx`. -| Path | Component | Access | Description | +| Path | Component | Доступ | Описание | |---|---|---|---| | `/` | `HomePage` | Public | Главная страница | | `/stocks/:secid` | `StockPage` | Public | Страница акции | @@ -20,7 +20,7 @@ - `
` с максимальной шириной 1200px - `` для контента страницы -## Route Structure +## Структура маршрутов ```tsx diff --git a/apps/docs/docs/frontend/styling.md b/apps/docs/docs/frontend/styling.md index b3353f0..c566103 100644 --- a/apps/docs/docs/frontend/styling.md +++ b/apps/docs/docs/frontend/styling.md @@ -1,10 +1,10 @@ -# Styling +# Стили -## Approach +## Подход CSS через единый `styles.css` с CSS custom properties. Без CSS-in-JS или Tailwind. -## CSS Custom Properties +## CSS custom properties Определены в `styles.css`: diff --git a/apps/docs/docs/getting-started.md b/apps/docs/docs/getting-started.md index a814388..53d4943 100644 --- a/apps/docs/docs/getting-started.md +++ b/apps/docs/docs/getting-started.md @@ -1,24 +1,24 @@ -# Getting Started +# Быстрый старт -## Prerequisites +## Требования - Node.js 20+ - npm 9+ -## Quick Start +## Запуск для разработки ```bash -# 1. Clone repository +# 1. Клонировать репозиторий git clone cd moex-vibe -# 2. Install dependencies +# 2. Установить зависимости npm install -# 3. Start backend (http://localhost:3000) +# 3. Запустить backend (http://localhost:3000) npm run dev:backend -# 4. In another terminal — start frontend (http://localhost:5173) +# 4. В другом терминале запустить frontend (http://localhost:5173) npm run dev:frontend ``` @@ -34,9 +34,9 @@ docker compose up --build - Backend: http://localhost:3000 - Swagger UI: http://localhost:3000/api/docs -## Verify +## Проверка -Open http://localhost:5173 (or http://localhost:80 for Docker). You should see: +Откройте http://localhost:5173 или http://localhost:80 при запуске через Docker. Должны быть доступны: 1. Страница с поисковой строкой и заголовком "MoexVibe" 2. Swagger UI на http://localhost:3000/api/docs со всеми endpoint'ами diff --git a/apps/docs/docs/infrastructure/ci.md b/apps/docs/docs/infrastructure/ci.md index 34bc024..80a8723 100644 --- a/apps/docs/docs/infrastructure/ci.md +++ b/apps/docs/docs/infrastructure/ci.md @@ -1,6 +1,6 @@ # CI/CD -## CI Pipeline +## CI pipeline Определён в `.gitea/workflows/ci.yml`. Запускается на push/PR в `main`. diff --git a/apps/docs/docs/infrastructure/docker.md b/apps/docs/docs/infrastructure/docker.md index 6ee37d6..28221a8 100644 --- a/apps/docs/docs/infrastructure/docker.md +++ b/apps/docs/docs/infrastructure/docker.md @@ -1,10 +1,10 @@ # Docker -## Architecture +## Архитектура ```mermaid graph TD - User["User"] + User["Пользователь"] Nginx["Nginx :80"] Backend["Node.js :3000"] MOEX["MOEX ISS
iss.moex.com"] @@ -15,7 +15,7 @@ graph TD Backend -->|"API calls"| MOEX ``` -## Services +## Сервисы ### Backend (`Dockerfile.backend`) @@ -57,7 +57,7 @@ CMD ["nginx", "-g", "daemon off;"] Собирает статику, затем раздаёт через Nginx. -### Nginx Config (`nginx.conf`) +### Конфигурация Nginx (`nginx.conf`) ```nginx server { @@ -105,7 +105,7 @@ services: - backend ``` -## Run +## Запуск ```bash docker compose up --build diff --git a/apps/docs/docs/intro.md b/apps/docs/docs/intro.md index 40e3ccc..e824d09 100644 --- a/apps/docs/docs/intro.md +++ b/apps/docs/docs/intro.md @@ -6,21 +6,21 @@ slug: / Веб-приложение для анализа ценных бумаг Московской биржи (MOEX). -## Tech Stack +## Технологический стек - **Backend:** NestJS, TypeScript, OpenAPI (Swagger) - **Frontend:** React 18, TypeScript, Vite, TanStack Query v5, lightweight-charts v4 -- **Infrastructure:** Docker, docker-compose +- **Инфраструктура:** Docker, docker-compose - **CI:** Gitea Actions -## Repository Structure +## Структура репозитория ``` moex-vibe/ ├── apps/ │ ├── backend/ # NestJS API (единственная точка доступа к MOEX) │ ├── frontend/ # React SPA -│ └── docs/ # Docusaurus documentation site +│ └── docs/ # Docusaurus-сайт документации ├── docs/ │ └── superpowers/ │ └── specs/ # Согласованные SDD-спецификации @@ -32,7 +32,7 @@ moex-vibe/ └── tsconfig.base.json ``` -## Key Principles +## Ключевые принципы - Backend — единственный клиент MOEX. Frontend никогда не обращается к MOEX напрямую. - npm workspaces монорепозиторий: `apps/backend`, `apps/frontend` и `apps/docs`. diff --git a/apps/docs/docusaurus.config.ts b/apps/docs/docusaurus.config.ts index 7d2308c..1f331f6 100644 --- a/apps/docs/docusaurus.config.ts +++ b/apps/docs/docusaurus.config.ts @@ -4,7 +4,7 @@ import type * as Preset from '@docusaurus/preset-classic'; const config: Config = { title: 'MoexVibe', - tagline: 'Analysis of MOEX securities', + tagline: 'Анализ ценных бумаг MOEX', favicon: 'img/favicon.ico', url: 'https://moexvibe.example.com', diff --git a/apps/docs/sidebars.ts b/apps/docs/sidebars.ts index 9ae9dda..2d11a82 100644 --- a/apps/docs/sidebars.ts +++ b/apps/docs/sidebars.ts @@ -35,12 +35,12 @@ const sidebars: SidebarsConfig = { }, { type: 'category', - label: 'Infrastructure', + label: 'Инфраструктура', items: ['infrastructure/docker', 'infrastructure/ci'], }, { type: 'category', - label: 'Development', + label: 'Разработка', items: [ 'development/commands', 'development/testing', @@ -50,7 +50,7 @@ const sidebars: SidebarsConfig = { }, { type: 'category', - label: 'Architecture Decisions (ADR)', + label: 'Архитектурные решения (ADR)', items: [ 'adr/index', 'adr/ADR-001-backend-single-point-of-access', diff --git a/docs/superpowers/specs/2026-06-15-russian-docs-and-architecture-diagrams.md b/docs/superpowers/specs/2026-06-15-russian-docs-and-architecture-diagrams.md new file mode 100644 index 0000000..ea299df --- /dev/null +++ b/docs/superpowers/specs/2026-06-15-russian-docs-and-architecture-diagrams.md @@ -0,0 +1,212 @@ +# Русификация документации и исправление архитектурных схем — SDD-спецификация + +**Дата:** 2026-06-15 + +**Статус:** черновик для ревью + +## Контекст + +Документация проекта публикуется только из `apps/docs` через Docusaurus. Текущая структура уже +зафиксирована в `docs/superpowers/specs/2026-06-13-docusaurus-docs-design.md`: страницы лежат в +`apps/docs/docs`, навигация описана в `apps/docs/sidebars.ts`, Mermaid включён через +`@docusaurus/theme-mermaid`. + +Сейчас большая часть человекочитаемого текста в опубликованной документации написана на английском: +заголовки, подписи таблиц, описания endpoint'ов, ADR summary, названия разделов sidebar. Проект +ведётся на русском, поэтому документация должна быть русскоязычной, оставляя на английском только +технические названия, идентификаторы и общепринятые инженерные термины. + +На странице `apps/docs/docs/architecture.md` первая Mermaid-схема `System Architecture` смешивает +в одном `graph TD` внешний контур приложения и внутреннее устройство backend. Из-за этого Docusaurus +рендерит слишком широкую и высокую схему: блоки и подписи стрелок визуально накладываются друг на +друга, особенно вокруг `Backend (NestJS)`, `Browser (React SPA)`, `NestJS API`, cache, MOEX и SQLite. + +## Цели + +1. Перевести опубликованную человекочитаемую документацию в `apps/docs` на русский язык. +2. Оставить без перевода технические названия и идентификаторы, которые должны совпадать с кодом, + API, библиотеками или файлами. +3. Исправить читаемость Mermaid-схем в `apps/docs/docs/architecture.md`, убрав наложение блоков и + подписей. +4. Сохранить текущую структуру Docusaurus и URL/slugs, чтобы не ломать существующие ссылки. +5. Задать проверяемые критерии приёмки для последующей реализации. + +## Не цели + +- Не менять backend, frontend, OpenAPI contract, runtime-конфигурацию или Docker-инфраструктуру. +- Не переименовывать файлы документации, ADR-файлы, route slugs и ссылки между страницами, если это + не требуется для исправления битой ссылки. +- Не внедрять i18n с несколькими локалями: сайт остаётся одноязычным с `defaultLocale: 'ru'`. +- Не переписывать архитектурные решения по сути. ADR переводятся как документация, но их смысл, + статус и последствия сохраняются. +- Не менять визуальную тему Docusaurus сверх минимальной поддержки читаемых Mermaid-схем. + +## Область изменений + +### Опубликованная документация + +Перевод и редактура затрагивают: + +- `apps/docs/docusaurus.config.ts` +- `apps/docs/sidebars.ts` +- `apps/docs/docs/intro.md` +- `apps/docs/docs/getting-started.md` +- `apps/docs/docs/architecture.md` +- `apps/docs/docs/backend/*.md` +- `apps/docs/docs/frontend/*.md` +- `apps/docs/docs/development/*.md` +- `apps/docs/docs/infrastructure/*.md` +- `apps/docs/docs/adr/*.md` + +### Стили + +`apps/docs/src/css/custom.css` можно менять только если после разбиения Mermaid-схем остаются +проблемы с шириной, переносами или горизонтальной прокруткой. CSS не должен быть основным способом +исправления сломанной схемы. + +## Правила русификации + +### Переводить + +- Заголовки страниц и разделов: `Architecture` -> `Архитектура`, `Getting Started` -> `Быстрый старт`. +- Sidebar labels: `Infrastructure` -> `Инфраструктура`, `Development` -> `Разработка`, + `Architecture Decisions (ADR)` -> `Архитектурные решения (ADR)`. +- Описания таблиц и колонок: `Description` -> `Описание`, `Required` -> `Обязателен`, + `Default` -> `По умолчанию`. +- Поясняющий текст, инструкции, списки, summaries, примечания к flow/sequence diagrams. +- Подписи Mermaid-стрелок, если они не являются точным API, cookie/header или именем метода: + `fetches` -> `запрашивает`, `uses` -> `использует`, `raw data` -> `сырые данные`. +- Ошибочные или неактуальные формулировки, найденные при переводе, если их можно исправить по + текущему коду или уже существующим SDD/ADR. + +### Не переводить + +- Названия технологий и библиотек: `NestJS`, `React`, `Vite`, `TanStack Query`, `Prisma`, + `SQLite`, `Docusaurus`, `Docker`, `Nginx`, `Vitest`, `MSW`, `Swagger`, `OpenAPI`, + `lightweight-charts`, `openapi-fetch`. +- Архитектурные технические существительные, уже используемые в проекте как термины: `backend`, `frontend`, + `middleware`, `endpoint`, `request`, `response`, `cache`, `cookie`, `access token`, + `refresh token`, `rate limiter`, `circuit breaker`, `codegen`, `workspace`, `build`, `lint`. +- Имена модулей, классов, DTO, методов, env vars, scripts, файлов и директорий: + `AuthModule`, `MoexClientService`, `CACHE_MARKET_DATA_TTL`, `npm run dev:backend`, + `apps/backend/src/modules/auth`. +- HTTP methods, URL paths, JSON keys, TypeScript/Prisma identifiers, enum values и code blocks. +- ADR file names and IDs: `ADR-001-backend-single-point-of-access.md`, `ADR-010`, etc. + +### Стиль русского текста + +- Писать для разработчика проекта, без маркетингового тона. +- Использовать короткие предложения и конкретные глаголы. +- Сохранять технические термины в одном написании по всему сайту. +- Не переводить термин, если перевод ухудшает связь с кодом или общепринятой практикой. + +## План исправления `architecture.md` + +Текущую первую схему нужно заменить набором меньших схем: + +1. `Архитектура системы` — внешний контур: Browser/React SPA, NestJS API, SQLite, cache, + MOEX ISS API. Направление лучше сделать `flowchart LR`, чтобы схема читалась слева направо. +2. `Внутренние модули backend` — отдельная схема только для NestJS feature/global modules: + AuthModule, SharesModule, BondsModule, CandlesModule, SecuritiesModule, PortfolioModule, + HealthModule, MoexClientModule, CacheModule/CacheService, PrismaModule/PrismaService. +3. `Поток авторизованного запроса` — существующий `sequenceDiagram` оставить как отдельный + сценарий, но перевести человекочитаемые подписи и сверить путь запроса с актуальными docs/API. + +Для первой схемы недопустимо помещать большой `subgraph Backend_Internal` внутрь графа внешней +архитектуры. Если нужно показать, что `NestJS API` состоит из модулей, это должно быть ссылкой +текстом или отдельной диаграммой ниже. + +## Рекомендуемый Mermaid-паттерн + +```mermaid +flowchart LR + Browser["Browser
(React SPA)"] + API["NestJS API
:3000"] + Cache["In-memory cache
(cache-manager)"] + MOEX["MOEX ISS API
iss.moex.com"] + DB[("SQLite
(Prisma)")] + + Browser -->|"/api/v1/*"| API + Browser -->|"Authorization: Bearer"| API + Browser -->|"Cookie: refreshToken"| API + API -->|"getOrFetch()"| Cache + API -->|"GET /iss/*.json"| MOEX + API -->|"Prisma ORM"| DB + Cache -->|"данные"| API + MOEX -->|"сырые данные"| API + DB -->|"пользователи и портфели"| API +``` + +В реализации можно менять конкретное расположение узлов, если итоговая схема проходит визуальную +проверку и остаётся семантически эквивалентной. + +## План реализации + +1. Создать отдельную ветку `codex/russian-docs-architecture-diagrams`, если текущая ветка не + предназначена для этой работы. +2. Пройти по navigation/config files: + `apps/docs/docusaurus.config.ts` и `apps/docs/sidebars.ts`. +3. Перевести overview-страницы: + `intro.md`, `getting-started.md`, `architecture.md`. +4. В `architecture.md` заменить первую Mermaid-схему на две небольшие схемы и перевести + человекочитаемые подписи в `sequenceDiagram`. +5. Перевести backend-раздел, сохраняя имена модулей, endpoints, DTO, env vars и code examples. +6. Перевести frontend-раздел, сохраняя названия React/Vite/TanStack Query и имена hooks/components. +7. Перевести infrastructure/development-разделы, сохраняя команды, scripts, имена workflow jobs, + Dockerfile paths и package names. +8. Перевести ADR index и ADR pages без изменения сути решений, статусов и ссылок. +9. Проверить Markdown-ссылки, таблицы и fenced code blocks. +10. Запустить `npm run build:docs`. +11. Если build проходит, открыть локальный Docusaurus и визуально проверить `architecture.md`: + нет наложения блоков, подписи стрелок читаемы, схема не уезжает за viewport на desktop. + +## Критерии приёмки + +- Все опубликованные страницы в `apps/docs/docs/**/*.md` имеют русские заголовки и русский + человекочитаемый текст, кроме разрешённых технических названий. +- `apps/docs/sidebars.ts` показывает русские labels для всех человекочитаемых категорий. +- `apps/docs/docusaurus.config.ts` содержит русскую tagline или нейтральную русскоязычную фразу. +- Имена файлов, route slugs, ADR IDs, endpoint paths, env vars, code blocks и API examples не + переименованы ради перевода. +- `apps/docs/docs/architecture.md` не содержит одной большой Mermaid-схемы, которая одновременно + показывает внешний контур и внутренние backend modules. +- Mermaid-схемы на странице `architecture.md` рендерятся без наложения узлов и подписей. +- `npm run build:docs` завершается успешно. +- После сборки нет новых broken links или broken Markdown links сверх текущей политики + `onBrokenLinks: 'warn'`, `onBrokenMarkdownLinks: 'warn'`. + +## Проверка качества + +### Автоматическая + +```bash +npm run build:docs +``` + +Ожидаемый результат: Docusaurus build завершается без ошибки. + +### Ручная + +1. Запустить `npm run dev:docs`. +2. Открыть страницу `/architecture`. +3. Проверить, что первая схема помещается в контентную область на desktop без наложения. +4. Проверить, что sequence diagram читается и подписи на русском там, где они не являются + техническими идентификаторами. +5. Выборочно открыть по одной странице из разделов Backend, Frontend, Инфраструктура, Разработка и + ADR, чтобы подтвердить единый стиль перевода. + +## Риски и решения + +| Риск | Решение | +|---|---| +| Чрезмерный перевод ломает связь с кодом | Следовать списку "Не переводить" и оставлять code/API terms как есть | +| Mermaid всё ещё строит слишком широкую схему | Делить диаграмму ещё мельче, а не пытаться лечить layout только CSS | +| ADR после перевода могут звучать как новые решения | Сохранять ID, статус, контекст, consequences и не менять смысл | +| Перевод таблиц может сломать Markdown | После каждой группы страниц запускать build или локально просматривать diff | + +## Решение для ревью + +Рекомендуемый путь: сначала выполнить русификацию и разбиение схем без изменения информационной +архитектуры сайта. После этого отдельно оценить, нужно ли делать полноценную редакторскую +нормализацию терминов или добавлять глоссарий. Для текущей задачи глоссарий в отдельной странице +не нужен: достаточно единых правил в этой спецификации и аккуратного применения по docs.