docs: add Docusaurus documentation site as npm workspace
All checks were successful
All checks were successful
- Create apps/docs/ with Docusaurus 3.7.0 - Add documentation: architecture, backend, frontend, infrastructure, development, ADR - Include 5 Mermaid diagrams (system architecture, request flow, modules, caching, deployment) - Configure as npm workspace with dev:docs/build:docs scripts - Copy existing ADR documents from docs/architecture/adr/
This commit is contained in:
parent
a6d4b25afe
commit
aac5b2f873
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,3 +6,5 @@ dist/
|
||||
*.tsbuildinfo
|
||||
vite.config.d.ts
|
||||
vite.config.js
|
||||
apps/docs/.docusaurus/
|
||||
apps/docs/build/
|
||||
|
||||
3
apps/docs/babel.config.js
Normal file
3
apps/docs/babel.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
||||
25
apps/docs/docs/adr/ADR-001-backend-single-point-of-access.md
Normal file
25
apps/docs/docs/adr/ADR-001-backend-single-point-of-access.md
Normal file
@ -0,0 +1,25 @@
|
||||
# ADR-001: Backend — Single Point of Access to MOEX
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-06-13
|
||||
**Deciders:** Architect, Tech Lead
|
||||
|
||||
## Context
|
||||
Frontend должен отображать данные Московской биржи. MOEX ISS API отдаёт сырые данные со сложной структурой (вложенные таблицы, различные форматы). Прямые запросы с фронта приведут к дублированию логики нормализации, усложнят обработку ошибок и сделают систему зависимой от внешнего API.
|
||||
|
||||
## Decision
|
||||
Backend (NestJS) является единственной точкой доступа к MOEX. Frontend никогда не обращается к MOEX напрямую.
|
||||
|
||||
Backend:
|
||||
- Проксирует запросы к MOEX ISS
|
||||
- Нормализует данные в доменные модели
|
||||
- Кеширует ответы
|
||||
- Обрабатывает ошибки MOEX (пустые данные, rate limit, таймауты)
|
||||
- Предоставляет собственный OpenAPI-контракт для фронта
|
||||
|
||||
## Consequences
|
||||
- Единый источник правды для трансформации данных
|
||||
- Изоляция изменений MOEX API — меняется только MoexClient
|
||||
- Централизованное кеширование сокращает количество запросов к MOEX
|
||||
- Фронтенд остаётся тонким клиентом
|
||||
- Дополнительная задержка (один hop), но нивелируется кешированием
|
||||
33
apps/docs/docs/adr/ADR-002-in-memory-cache.md
Normal file
33
apps/docs/docs/adr/ADR-002-in-memory-cache.md
Normal file
@ -0,0 +1,33 @@
|
||||
# ADR-002: In-Memory Cache with Migration Path to Redis
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-06-13
|
||||
**Deciders:** Architect, Tech Lead
|
||||
|
||||
## Context
|
||||
Для MVP требуется кеширование MOEX-данных, чтобы снизить нагрузку на внешнее API и обеспечить приемлемое время ответа. На начальном этапе нет требований к горизонтальному масштабированию, и хочется избежать внешних зависимостей.
|
||||
|
||||
## Decision
|
||||
Использовать `@nestjs/cache-manager` с MemoryStore. TTL настраивается per-endpoint через конфигурацию.
|
||||
|
||||
Архитектура позволяет переключиться на Redis заменой импорта провайдера:
|
||||
|
||||
```typescript
|
||||
// Текущая реализация
|
||||
CacheModule.register({ store: 'memory', ttl: 900 })
|
||||
|
||||
// Миграция на Redis (меняется только registration)
|
||||
CacheModule.registerAsync({
|
||||
useFactory: () => ({
|
||||
store: redisStore,
|
||||
host: process.env.REDIS_HOST,
|
||||
port: process.env.REDIS_PORT,
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
## Consequences
|
||||
- Нет внешних зависимостей для MVP
|
||||
- Кеш сбрасывается при рестарте сервера (приемлемо для read-only приложения)
|
||||
- Чистый путь миграции на Redis
|
||||
- Единый API для cache (cache-manager abstraction)
|
||||
21
apps/docs/docs/adr/ADR-003-rate-limiting-strategy.md
Normal file
21
apps/docs/docs/adr/ADR-003-rate-limiting-strategy.md
Normal file
@ -0,0 +1,21 @@
|
||||
# ADR-003: Rate Limiting Strategy for MOEX Client
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-06-13
|
||||
**Deciders:** Architect, Tech Lead
|
||||
|
||||
## Context
|
||||
MOEX ISS не документирует жёсткие лимиты на количество запросов, но массовые запросы могут привести к блокировке или ухудшению качества обслуживания. Backend является единственным клиентом MOEX и должен контролировать исходящий трафик.
|
||||
|
||||
## Decision
|
||||
Внедрить два механизма в MoexClient:
|
||||
|
||||
1. **Request Queue (p-queue)**: конфигурируемый лимит запросов в секунду (default: 10 req/s). Запросы сверх лимита ставятся в очередь и выполняются по расписанию.
|
||||
|
||||
2. **Circuit Breaker (`@nestjs/axios` + interceptor)**: при 5+ последовательных ошибках (5xx, timeout, network error) клиент перестаёт отправлять запросы к MOEX на 30 секунд. После таймаута — пробный запрос для восстановления.
|
||||
|
||||
## Consequences
|
||||
- Плавная нагрузка на MOEX, без пиков
|
||||
- Автоматическое восстановление после сбоев MOEX
|
||||
- Graceful degradation: при отключённом circuit breaker возвращаются кешированные данные
|
||||
- Параметр конфигурации `MOEX_RATE_LIMIT` (int, req/s)
|
||||
29
apps/docs/docs/adr/ADR-004-feature-modules.md
Normal file
29
apps/docs/docs/adr/ADR-004-feature-modules.md
Normal file
@ -0,0 +1,29 @@
|
||||
# ADR-004: Feature Modules by Domain
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-06-13
|
||||
**Deciders:** Architect, Tech Lead
|
||||
|
||||
## Context
|
||||
NestJS рекомендует модульную архитектуру. Требования указывают на архитектуру по feature modules. Модули должны иметь чёткие границы и быть тестируемыми изолированно.
|
||||
|
||||
## Decision
|
||||
Каждый бизнес-домен — отдельный NestJS feature module:
|
||||
|
||||
| Module | Responsibility |
|
||||
|--------|---------------|
|
||||
| `MoexClientModule` | HTTP-клиент к MOEX ISS, rate limiting, circuit breaker |
|
||||
| `CacheModule` | Абстракция кеширования |
|
||||
| `SecuritiesModule` | Поиск по инструментам |
|
||||
| `SharesModule` | Спецификация, marketdata, дивиденды |
|
||||
| `BondsModule` | Спецификация, marketdata |
|
||||
| `CandlesModule` | OHLCV свечи (общий для shares+bonds) |
|
||||
| `HealthModule` | Healthcheck endpoint |
|
||||
|
||||
Каждый module exports свой сервис, control imports через `@Module({ imports: [...] })`.
|
||||
|
||||
## Consequences
|
||||
- Чёткие границы, изолированное тестирование
|
||||
- Возможность вынести модуль в отдельный микросервис
|
||||
- Понятная навигация по коду
|
||||
- Нет циклических зависимостей (MoexClient — единственный downstream)
|
||||
40
apps/docs/docs/adr/ADR-005-openapi-codegen-frontend.md
Normal file
40
apps/docs/docs/adr/ADR-005-openapi-codegen-frontend.md
Normal file
@ -0,0 +1,40 @@
|
||||
# ADR-005: OpenAPI Codegen with openapi-typescript
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-06-13
|
||||
**Deciders:** Architect, Tech Lead
|
||||
|
||||
## Context
|
||||
Frontend должен потреблять API бэкенда. Ручное написание клиентов и DTO приводит к рассинхронизации с бэкендом и ошибкам типизации.
|
||||
|
||||
## Decision
|
||||
Использовать `openapi-typescript` + `openapi-fetch` для генерации:
|
||||
|
||||
- TypeScript типов (DTO, request/response schemas)
|
||||
- Fetcher клиента (типобезопасные вызовы)
|
||||
|
||||
Процесс:
|
||||
1. Backend генерирует OpenAPI spec через `@nestjs/swagger`
|
||||
2. `openapi-typescript` на фронте генерирует типы
|
||||
3. `openapi-fetch` создаёт типобезопасный HTTP-клиент
|
||||
4. Разработчик пишет TanStack Query hooks вручную поверх сгенерированного клиента
|
||||
|
||||
```typescript
|
||||
// Пример: типобезопасный хук
|
||||
import { getSharesSecid } from '@/api/client';
|
||||
import type { components } from '@/api/types';
|
||||
|
||||
export function useStock(secid: string) {
|
||||
return useQuery({
|
||||
queryKey: ['stock', secid],
|
||||
queryFn: () => getSharesSecid(secid),
|
||||
staleTime: 900_000, // 15 min
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Consequences
|
||||
- Полная типобезопасность на стыке frontend/backend
|
||||
- Автоматическая синхронизация с API-контрактом
|
||||
- TanStack Query hooks пишутся вручную — полный контроль staleTime/caching
|
||||
- Добавляется шаг в CI: codegen при изменении OpenAPI spec
|
||||
20
apps/docs/docs/adr/ADR-006-no-cci.md
Normal file
20
apps/docs/docs/adr/ADR-006-no-cci.md
Normal file
@ -0,0 +1,20 @@
|
||||
# ADR-006: CCI (Financial Reporting) Moved Out of MVP
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-06-13
|
||||
**Deciders:** Architect, Product
|
||||
|
||||
## Context
|
||||
MOEX предоставляет корпоративную информацию (CCI) — финансовую отчётность по МСФО/РСБУ. Данные включают отчёты о прибылях/убытках, балансовые отчёты, мультипликаторы. Однако:
|
||||
|
||||
- CCI API имеет собственную сложную структуру (виды отчётности, периоды, индикаторы)
|
||||
- Данные требуют дополнительной нормализации и расчёта метрик
|
||||
- Для MVP пользователи хотят базовую информацию (цена, купон, график)
|
||||
|
||||
## Decision
|
||||
Не включать CCI в MVP. Roadmap на post-MVP.
|
||||
|
||||
## Consequences
|
||||
- Меньший объём работы в MVP
|
||||
- API не привязывается к CCI-схемам (будет отдельный модуль)
|
||||
- Пользователи не увидят мультипликаторы (P/E, EV/EBITDA) в первой версии
|
||||
27
apps/docs/docs/adr/ADR-007-two-level-caching.md
Normal file
27
apps/docs/docs/adr/ADR-007-two-level-caching.md
Normal file
@ -0,0 +1,27 @@
|
||||
# ADR-007: Two-Level Caching (Backend + Frontend)
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-06-13
|
||||
**Deciders:** Architect
|
||||
|
||||
## Context
|
||||
Данные MOEX имеют задержку 15 минут. Кеширование на одном уровне (только бэкенд или только фронтенд) неоптимально:
|
||||
- Только бэкенд: каждый пользователь создаёт запрос к серверу
|
||||
- Только фронтенд: нет централизованного кеша, не защищает MOEX от повторных запросов
|
||||
|
||||
## Decision
|
||||
Внедрить два уровня кеширования:
|
||||
|
||||
1. **Backend (in-memory cache-manager)**: централизованное кеширование ответов от MOEX. Предотвращает повторные запросы к MOEX от разных пользователей.
|
||||
|
||||
2. **Frontend (TanStack Query staleTime)**: предотвращает повторные запросы к бэкенду при навигации или монтировании компонентов.
|
||||
|
||||
TTL согласованы (см. Caching Strategy).
|
||||
|
||||
Cache-Control заголовки в HTTP-ответах для промежуточных proxy/CDN (опционально).
|
||||
|
||||
## Consequences
|
||||
- Избыточность intentional: resilience при отказе одного уровня
|
||||
- TanStack Query staleTime = backend TTL (нет лишних запросов)
|
||||
- При рестарте бэкенда фронт всё ещё имеет данные в memory cache
|
||||
- Небольшое увеличение memory на фронте (приемлемо для SPA)
|
||||
13
apps/docs/docs/adr/index.md
Normal file
13
apps/docs/docs/adr/index.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Architecture Decision Records
|
||||
|
||||
| ADR | Status | Description |
|
||||
|---|---|---|
|
||||
| [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 находятся в `docs/architecture/adr/`.
|
||||
110
apps/docs/docs/architecture.md
Normal file
110
apps/docs/docs/architecture.md
Normal file
@ -0,0 +1,110 @@
|
||||
# Architecture
|
||||
|
||||
## System Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Browser["Browser<br/>(React SPA)"]
|
||||
Backend["NestJS API<br/>:3000"]
|
||||
Cache["In-Memory Cache<br/>(cache-manager)"]
|
||||
MOEX["MOEX ISS API<br/>iss.moex.com"]
|
||||
|
||||
Browser -->|"/api/v1/*"| Backend
|
||||
Backend -->|"getOrFetch()"| Cache
|
||||
Backend -->|"GET /iss/*.json"| MOEX
|
||||
Cache -->|"data"| Backend
|
||||
MOEX -->|"raw data"| Backend
|
||||
|
||||
subgraph Backend_Internal["Backend (NestJS)"]
|
||||
MoexClient["MoexClientModule<br/>p-queue + circuit breaker"]
|
||||
Shares["SharesModule"]
|
||||
Bonds["BondsModule"]
|
||||
Candles["CandlesModule"]
|
||||
Securities["SecuritiesModule"]
|
||||
Health["HealthModule"]
|
||||
CacheService["CacheService<br/>(global)"]
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## Request Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant Frontend as React SPA
|
||||
participant Backend as NestJS API
|
||||
participant Cache as In-Memory Cache
|
||||
participant MOEX as MOEX ISS
|
||||
|
||||
User->>Frontend: Search / View instrument
|
||||
Frontend->>Backend: GET /api/v1/securities/search?q=SBER
|
||||
Backend->>Cache: getOrFetch('search:sber')
|
||||
alt Cache miss
|
||||
Cache->>Backend: null
|
||||
Backend->>MOEX: GET /iss/securities?q=SBER
|
||||
MOEX-->>Backend: raw data
|
||||
Backend->>Cache: set('search:sber', normalized, TTL=3600)
|
||||
else Cache hit
|
||||
Cache-->>Backend: cached data
|
||||
end
|
||||
Backend-->>Frontend: { data, meta: { fromCache, cachedAt } }
|
||||
Frontend-->>User: rendered UI
|
||||
```
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
All architectural decisions are documented as ADR in `docs/architecture/adr/`:
|
||||
|
||||
| ADR | Summary |
|
||||
|---|---|
|
||||
| [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 |
|
||||
|
||||
## Response Format
|
||||
|
||||
Все ответы API обёрнуты в единый формат:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": { ... },
|
||||
"meta": {
|
||||
"fromCache": false,
|
||||
"cachedAt": "2026-06-13T12:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
При ошибках возвращается:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusCode": 404,
|
||||
"message": "Share SBER not found",
|
||||
"error": "Not Found",
|
||||
"timestamp": "2026-06-13T12:00:00.000Z",
|
||||
"path": "/api/v1/securities/shares/SBER"
|
||||
}
|
||||
```
|
||||
|
||||
## Global Configuration
|
||||
|
||||
- Global prefix: `/api/v1`
|
||||
- ValidationPipe: `transform: true, whitelist: true`
|
||||
- HttpExceptionFilter (catch-all)
|
||||
- TransformInterceptor (авто-обёртка в `ApiResponse`)
|
||||
- RequestLoggingMiddleware (логирует `METHOD /path STATUS DURATIONms`)
|
||||
- CORS: разрешён для всех origins
|
||||
275
apps/docs/docs/backend/api.md
Normal file
275
apps/docs/docs/backend/api.md
Normal file
@ -0,0 +1,275 @@
|
||||
# API Reference
|
||||
|
||||
Все эндпоинты находятся под префиксом `/api/v1`. Swagger UI: `/api/docs`.
|
||||
|
||||
## Health
|
||||
|
||||
### `GET /health`
|
||||
|
||||
Проверка состояния сервиса.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"timestamp": "2026-06-13T12:00:00.000Z",
|
||||
"uptime": 1234.56
|
||||
}
|
||||
```
|
||||
|
||||
## Securities
|
||||
|
||||
### `GET /securities/search`
|
||||
|
||||
Поиск по инструментам.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Param | Type | Required | Default | Description |
|
||||
|---|---|---|---|---|
|
||||
| `q` | string | yes | — | Поисковый запрос (1-100 символов) |
|
||||
| `type` | enum | no | `all` | Фильтр: `all`, `share`, `bond` |
|
||||
| `limit` | integer | no | `20` | Лимит результатов |
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"secid": "SBER",
|
||||
"isin": "RU0009029540",
|
||||
"shortName": "Сбербанк",
|
||||
"type": "share",
|
||||
"listLevel": 1,
|
||||
"currency": "RUB",
|
||||
"price": null
|
||||
}
|
||||
],
|
||||
"meta": { "fromCache": false, "cachedAt": null }
|
||||
}
|
||||
```
|
||||
|
||||
## Shares
|
||||
|
||||
### `GET /securities/shares/:secid`
|
||||
|
||||
Спецификация акции.
|
||||
|
||||
**Parameters:** `secid` — тикер (например, `SBER`)
|
||||
|
||||
**Response:** `ShareResponse` — спецификация + текущие рыночные данные.
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"secid": "SBER",
|
||||
"isin": "RU0009029540",
|
||||
"name": "Сбербанк России ПАО ао",
|
||||
"shortName": "Сбербанк",
|
||||
"latName": null,
|
||||
"listLevel": 1,
|
||||
"issueSize": 21586948000,
|
||||
"faceValue": 3,
|
||||
"faceUnit": "RUB",
|
||||
"type": "common_share",
|
||||
"marketData": {
|
||||
"price": 322.35,
|
||||
"change": 1.15,
|
||||
"changePercent": 0.36,
|
||||
"open": 321.3,
|
||||
"high": 322.66,
|
||||
"low": 321.2,
|
||||
"volume": 1925163,
|
||||
"value": 620184479,
|
||||
"issueCapitalization": 6958336818320,
|
||||
"updatedAt": "2026-06-13T10:30:00.000Z"
|
||||
}
|
||||
},
|
||||
"meta": { "fromCache": false, "cachedAt": null }
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /securities/shares/:secid/marketdata`
|
||||
|
||||
Рыночные данные акции (без спецификации).
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"price": 322.35,
|
||||
"change": 1.15,
|
||||
"changePercent": 0.36,
|
||||
"open": 321.3,
|
||||
"high": 322.66,
|
||||
"low": 321.2,
|
||||
"volume": 1925163,
|
||||
"value": 620184479,
|
||||
"issueCapitalization": 6958336818320,
|
||||
"updatedAt": "2026-06-13T10:30:00.000Z"
|
||||
},
|
||||
"meta": { "fromCache": true, "cachedAt": null }
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /securities/shares/:secid/dividends`
|
||||
|
||||
Дивиденды акции.
|
||||
|
||||
**Parameters:** `secid` — тикер
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"registryCloseDate": "2026-07-10",
|
||||
"value": 33.4,
|
||||
"currency": "RUB"
|
||||
}
|
||||
],
|
||||
"meta": { "fromCache": false, "cachedAt": null }
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /securities/shares/:secid/history`
|
||||
|
||||
Дневная история торгов акции.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Param | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `from` | string (date) | yes | Начальная дата (`YYYY-MM-DD`) |
|
||||
| `till` | string (date) | yes | Конечная дата (`YYYY-MM-DD`) |
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"date": "2026-06-13",
|
||||
"open": 321.3,
|
||||
"high": 322.66,
|
||||
"low": 321.2,
|
||||
"close": 322.35,
|
||||
"volume": 1925163,
|
||||
"value": 620184479
|
||||
}
|
||||
],
|
||||
"meta": { "fromCache": false, "cachedAt": null }
|
||||
}
|
||||
```
|
||||
|
||||
## Bonds
|
||||
|
||||
### `GET /securities/bonds/:secid`
|
||||
|
||||
Спецификация облигации.
|
||||
|
||||
**Parameters:** `secid` — тикер
|
||||
|
||||
**Response:** `BondResponse` — спецификация + рыночные данные.
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"secid": "SU26238RMFS5",
|
||||
"isin": "RU000A106ZJ4",
|
||||
"name": "ОФЗ 26238",
|
||||
"shortName": "ОФЗ 26238",
|
||||
"latName": null,
|
||||
"listLevel": 1,
|
||||
"issueSize": 500000000,
|
||||
"faceValue": 1000,
|
||||
"faceUnit": "RUB",
|
||||
"matDate": "2041-05-15",
|
||||
"couponValue": 34.9,
|
||||
"couponPercent": 6.98,
|
||||
"couponPeriod": 182,
|
||||
"nextCoupon": "2026-12-01",
|
||||
"accruedInt": 12.45,
|
||||
"bondType": "OFZ",
|
||||
"bondSubType": "",
|
||||
"offerDate": null,
|
||||
"buybackDate": null,
|
||||
"marketData": {
|
||||
"price": 98.45,
|
||||
"yieldToMaturity": 7.12,
|
||||
"duration": 8.34,
|
||||
"accruedInt": 12.45,
|
||||
"couponValue": 34.9,
|
||||
"couponPercent": 6.98,
|
||||
"nextCouponDate": "2026-12-01",
|
||||
"open": 98.3,
|
||||
"high": 98.6,
|
||||
"low": 98.2,
|
||||
"volume": 1500000,
|
||||
"updatedAt": "2026-06-13T10:30:00.000Z"
|
||||
}
|
||||
},
|
||||
"meta": { "fromCache": false, "cachedAt": null }
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /securities/bonds/:secid/marketdata`
|
||||
|
||||
Рыночные данные облигации.
|
||||
|
||||
### `GET /securities/bonds/:secid/history`
|
||||
|
||||
Дневная история торгов облигации.
|
||||
|
||||
**Parameters:** `from`, `till` (date)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"date": "2026-06-13",
|
||||
"closePrice": 98.45,
|
||||
"yieldClose": 7.12,
|
||||
"duration": 8.34
|
||||
}
|
||||
],
|
||||
"meta": { "fromCache": false, "cachedAt": null }
|
||||
}
|
||||
```
|
||||
|
||||
## Candles
|
||||
|
||||
### `GET /securities/shares/:secid/candles`
|
||||
|
||||
Свечи акции.
|
||||
|
||||
### `GET /securities/bonds/:secid/candles`
|
||||
|
||||
Свечи облигации.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Param | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `interval` | enum | yes | `1h` или `24h` |
|
||||
| `from` | string (date) | yes | Начальная дата |
|
||||
| `till` | string (date) | yes | Конечная дата |
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"open": 321.3,
|
||||
"high": 322.66,
|
||||
"low": 321.2,
|
||||
"close": 322.35,
|
||||
"volume": 1925163,
|
||||
"value": 620184479,
|
||||
"begin": "2026-06-13T10:00:00",
|
||||
"end": "2026-06-13T10:59:59"
|
||||
}
|
||||
],
|
||||
"meta": { "fromCache": false, "cachedAt": null }
|
||||
}
|
||||
```
|
||||
80
apps/docs/docs/backend/caching.md
Normal file
80
apps/docs/docs/backend/caching.md
Normal file
@ -0,0 +1,80 @@
|
||||
# Caching
|
||||
|
||||
## Cache Strategy
|
||||
|
||||
In-memory кеш через `@nestjs/cache-manager` (cache-manager v5). Поддерживается миграция на Redis (см. ADR-002).
|
||||
|
||||
## Cache Flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Request["Request Data"]
|
||||
CacheService["CacheService.getOrFetch()"]
|
||||
BuildKey["buildKey(prefix, keyParts)"]
|
||||
CacheGet["cacheManager.get(key)"]
|
||||
Hit["Cache HIT → return data"]
|
||||
Miss["Cache MISS"]
|
||||
FetchFn["FetchFn() → get from MOEX"]
|
||||
CacheSet["cacheManager.set(key, data, ttl)"]
|
||||
Return["Return { data, fromCache, cachedAt }"]
|
||||
|
||||
Request --> CacheService
|
||||
CacheService --> BuildKey
|
||||
BuildKey --> CacheGet
|
||||
CacheGet --> Hit
|
||||
CacheGet --> Miss
|
||||
Miss --> FetchFn
|
||||
FetchFn --> CacheSet
|
||||
CacheSet --> Return
|
||||
Hit --> Return
|
||||
```
|
||||
|
||||
## CacheService
|
||||
|
||||
`apps/backend/src/modules/cache/cache.service.ts`
|
||||
|
||||
Основной метод:
|
||||
|
||||
```typescript
|
||||
getOrFetch<T>(
|
||||
keyPrefix: string, // 'marketdata' | 'history' | 'candles' | 'search' | 'bond' | 'dividends'
|
||||
keyParts: string[], // ['shares', 'SBER'] | ['SBER', '2026-06-13', '2026-06-14']
|
||||
fetchFn: () => Promise<T>,
|
||||
ttlConfigKey: string, // 'marketDataTtl' | 'historyTtl' | etc.
|
||||
): Promise<{ data: T; fromCache: boolean; cachedAt: string | null }>
|
||||
```
|
||||
|
||||
- Ключ строится как `keyPrefix:keyPart1:keyPart2:...`
|
||||
- При cache hit возвращает `{ data, fromCache: true, cachedAt: null }`
|
||||
- При cache miss вызывает `fetchFn`, сохраняет результат с TTL и возвращает `{ data, fromCache: false, cachedAt: '...' }`
|
||||
|
||||
## Cache Module Configuration
|
||||
|
||||
`apps/backend/src/modules/cache/cache.module.ts`:
|
||||
|
||||
```typescript
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
NestCacheModule.register({
|
||||
ttl: 900,
|
||||
max: 1000,
|
||||
isGlobal: true,
|
||||
}),
|
||||
],
|
||||
providers: [CacheService],
|
||||
exports: [CacheService],
|
||||
})
|
||||
export class CacheModule {}
|
||||
```
|
||||
|
||||
## Per-Data TTL
|
||||
|
||||
| Data Type | Config Key | Default TTL | Config Variable |
|
||||
|---|---|---|---|
|
||||
| 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` |
|
||||
45
apps/docs/docs/backend/configuration.md
Normal file
45
apps/docs/docs/backend/configuration.md
Normal file
@ -0,0 +1,45 @@
|
||||
# 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 |
|
||||
| `MOEX_RATE_LIMIT` | `10` | Максимум запросов в секунду к MOEX |
|
||||
| `MOEX_CIRCUIT_BREAKER_THRESHOLD` | `5` | Количество ошибок до открытия circuit breaker |
|
||||
| `MOEX_CIRCUIT_BREAKER_RESET_SECONDS` | `30` | Время в секундах до сброса circuit breaker |
|
||||
| `CACHE_MARKET_DATA_TTL` | `900` | TTL рыночных данных (секунды) |
|
||||
| `CACHE_HISTORY_TTL` | `3600` | TTL истории торгов (секунды) |
|
||||
| `CACHE_CANDLES_TTL` | `3600` | TTL свечей (секунды) |
|
||||
| `CACHE_SECURITY_TTL` | `86400` | TTL спецификации инструмента (секунды) |
|
||||
| `CACHE_SEARCH_TTL` | `3600` | TTL результатов поиска (секунды) |
|
||||
| `CACHE_DIVIDENDS_TTL` | `86400` | TTL дивидендов (секунды) |
|
||||
|
||||
## Configuration File
|
||||
|
||||
`apps/backend/src/config/configuration.ts`:
|
||||
|
||||
```typescript
|
||||
registerAs('app', () => ({
|
||||
port: parseInt(process.env.PORT || '3000', 10),
|
||||
moex: {
|
||||
baseUrl: process.env.MOEX_BASE_URL || 'https://iss.moex.com/iss',
|
||||
rateLimit: parseInt(process.env.MOEX_RATE_LIMIT || '10', 10),
|
||||
circuitBreakerThreshold: parseInt(process.env.MOEX_CIRCUIT_BREAKER_THRESHOLD || '5', 10),
|
||||
circuitBreakerResetSeconds: parseInt(
|
||||
process.env.MOEX_CIRCUIT_BREAKER_RESET_SECONDS || '30', 10,
|
||||
),
|
||||
},
|
||||
cache: {
|
||||
marketDataTtl: parseInt(process.env.CACHE_MARKET_DATA_TTL || '900', 10),
|
||||
historyTtl: parseInt(process.env.CACHE_HISTORY_TTL || '3600', 10),
|
||||
candlesTtl: parseInt(process.env.CACHE_CANDLES_TTL || '3600', 10),
|
||||
securityTtl: parseInt(process.env.CACHE_SECURITY_TTL || '86400', 10),
|
||||
searchTtl: parseInt(process.env.CACHE_SEARCH_TTL || '3600', 10),
|
||||
dividendsTtl: parseInt(process.env.CACHE_DIVIDENDS_TTL || '86400', 10),
|
||||
},
|
||||
}));
|
||||
```
|
||||
94
apps/docs/docs/backend/modules.md
Normal file
94
apps/docs/docs/backend/modules.md
Normal file
@ -0,0 +1,94 @@
|
||||
# Backend Modules
|
||||
|
||||
## Module Dependency Graph
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
AppModule --> ConfigModule
|
||||
AppModule --> CacheModule
|
||||
AppModule --> MoexClientModule
|
||||
AppModule --> HealthModule
|
||||
AppModule --> SecuritiesModule
|
||||
AppModule --> SharesModule
|
||||
AppModule --> BondsModule
|
||||
AppModule --> CandlesModule
|
||||
|
||||
subgraph Global_Modules["Global Modules"]
|
||||
CacheModule
|
||||
MoexClientModule
|
||||
end
|
||||
|
||||
SharesModule --> CacheService["CacheService"]
|
||||
BondsModule --> CacheService
|
||||
SecuritiesModule --> CacheService
|
||||
CandlesModule --> CacheService
|
||||
|
||||
SharesModule --> MoexClientService["MoexClientService"]
|
||||
BondsModule --> MoexClientService
|
||||
SecuritiesModule --> MoexClientService
|
||||
CandlesModule --> MoexClientService
|
||||
```
|
||||
|
||||
## Module List
|
||||
|
||||
| Module | Global | Path | Description |
|
||||
|---|---|---|---|
|
||||
| `CacheModule` | Yes | `modules/cache/` | In-memory cache (cache-manager) |
|
||||
| `MoexClientModule` | Yes | `modules/moex-client/` | HTTP-клиент MOEX ISS |
|
||||
| `HealthModule` | No | `modules/health/` | Health check endpoint |
|
||||
| `SecuritiesModule` | No | `modules/securities/` | Поиск инструментов |
|
||||
| `SharesModule` | No | `modules/shares/` | Акции |
|
||||
| `BondsModule` | No | `modules/bonds/` | Облигации |
|
||||
| `CandlesModule` | No | `modules/candles/` | Свечи OHLCV |
|
||||
|
||||
### CacheModule
|
||||
|
||||
Глобальный in-memory кеш.
|
||||
|
||||
- Использует `@nestjs/cache-manager` со стандартным TTL 900s, максимум 1000 записей
|
||||
- Предоставляет `CacheService` с методом `getOrFetch(prefix, keyParts, fetchFn, ttlConfigKey)`
|
||||
- TTL настраивается через env-переменные `CACHE_*_TTL`
|
||||
|
||||
### MoexClientModule
|
||||
|
||||
Глобальный HTTP-клиент для MOEX ISS.
|
||||
|
||||
- Rate limiter: p-queue (10 req/s по умолчанию, настраивается через `MOEX_RATE_LIMIT`)
|
||||
- Circuit breaker: открывается после 5 ошибок, сбрасывается через 30s
|
||||
- Все ответы нормализуются из табличного формата MOEX в доменные типы
|
||||
|
||||
### HealthModule
|
||||
|
||||
Проверка состояния сервиса.
|
||||
|
||||
- `GET /api/v1/health` → `{ status: 'ok', timestamp, uptime }`
|
||||
|
||||
### SecuritiesModule
|
||||
|
||||
Поиск по инструментам (акции и облигации).
|
||||
|
||||
- Единственный эндпоинт: `GET /securities/search`
|
||||
- Фильтрация по типу: `all`, `share`, `bond`
|
||||
- Проверяет `group` и `type` поля из MOEX для классификации
|
||||
|
||||
### SharesModule
|
||||
|
||||
Работа с акциями.
|
||||
|
||||
- 4 эндпоинта: спецификация, marketdata, дивиденды, история
|
||||
- Поддерживает board `TQBR` (основной режим торгов акций)
|
||||
|
||||
### BondsModule
|
||||
|
||||
Работа с облигациями.
|
||||
|
||||
- 3 эндпоинта: спецификация, marketdata, история
|
||||
- Поддерживает board `TQCB` (основной режим торгов облигаций)
|
||||
|
||||
### CandlesModule
|
||||
|
||||
Получение свечных данных (OHLCV).
|
||||
|
||||
- 2 эндпоинта: для акций и облигаций
|
||||
- Интервалы: `1h` (60 min) и `24h` (daily)
|
||||
- Маппинг: `1h` → MOEX interval 60, `24h` → MOEX interval 24
|
||||
84
apps/docs/docs/backend/moex-client.md
Normal file
84
apps/docs/docs/backend/moex-client.md
Normal file
@ -0,0 +1,84 @@
|
||||
# MOEX Client
|
||||
|
||||
## Overview
|
||||
|
||||
`MoexClientService` (`apps/backend/src/modules/moex-client/moex-client.service.ts`) — HTTP-клиент для MOEX ISS API.
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
Использует `p-queue`:
|
||||
|
||||
```typescript
|
||||
this.queue = new PQueue({
|
||||
interval: 1000,
|
||||
intervalCap: rateLimit, // 10 по умолчанию
|
||||
});
|
||||
```
|
||||
|
||||
Все запросы к MOEX проходят через очередь — не более `MOEX_RATE_LIMIT` запросов в секунду.
|
||||
|
||||
## Circuit Breaker
|
||||
|
||||
Состояние: закрыт → открыт → полуоткрыт (через таймаут).
|
||||
|
||||
```typescript
|
||||
private circuitOpen = false;
|
||||
private circuitErrorCount = 0;
|
||||
```
|
||||
|
||||
- После `MOEX_CIRCUIT_BREAKER_THRESHOLD` (5) последовательных ошибок — открывается
|
||||
- В открытом состоянии все запросы мгновенно падают с ошибкой `"Circuit breaker is open"`
|
||||
- Через `MOEX_CIRCUIT_BREAKER_RESET_SECONDS` (30) автоматически сбрасывается
|
||||
|
||||
## Request Method
|
||||
|
||||
```typescript
|
||||
private async request<T>(path: string, params?: Record<string, string>): Promise<T>
|
||||
```
|
||||
|
||||
- Добавляет `.json` к пути (MOEX JSON API)
|
||||
- Устанавливает `iss.meta=off` (отключает метаданные)
|
||||
- Таймаут: 10s
|
||||
|
||||
## Response Parsing
|
||||
|
||||
MOEX возвращает данные в табличном формате:
|
||||
|
||||
```json
|
||||
{
|
||||
"securities": {
|
||||
"columns": ["secid", "isin", "name", ...],
|
||||
"data": [["SBER", "RU0009029540", "Сбербанк", ...]]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Метод `extractTable` преобразует это в массив объектов по колонкам.
|
||||
|
||||
## Available MOEX Methods
|
||||
|
||||
| Method | MOEX Path | Description |
|
||||
|---|---|---|
|
||||
| `searchSecurities` | `/securities?q=` | Поиск инструментов |
|
||||
| `getSecurityDescription` | `/securities/{secid}` | Спецификация |
|
||||
| `getShareMarketData` | `/engines/stock/markets/shares/securities/{secid}` | Рыночные данные акции (board: TQBR) |
|
||||
| `getBondData` | `/engines/stock/markets/bonds/securities/{secid}` | Данные облигации (board: TQCB) |
|
||||
| `getBondMarketData` | `/engines/stock/markets/bonds/securities/{secid}` | Рыночные данные облигации |
|
||||
| `getDividends` | `/securities/{secid}/dividends` | Дивиденды |
|
||||
| `getCandles` | `/engines/{engine}/markets/{market}/securities/{secid}/candles` | Свечи |
|
||||
| `getHistory` | `/engines/stock/markets/shares/securities/{secid}` | История акций |
|
||||
| `getBondHistory` | `/engines/stock/markets/bonds/securities/{secid}` | История облигаций |
|
||||
|
||||
## MOEX ISS Types
|
||||
|
||||
Все MOEX-типы описаны в `apps/backend/src/modules/moex-client/moex-client.types.ts`:
|
||||
|
||||
- `MoexSecurityDescription` — описание инструмента
|
||||
- `MoexShareMarketData` — рыночные данные акции
|
||||
- `MoexBondData` — данные облигации
|
||||
- `MoexBondMarketData` — рыночные данные облигации
|
||||
- `MoexDividend` — дивиденд
|
||||
- `MoexCandle` — свеча (OHLCV)
|
||||
- `MoexHistoryEntry` — запись истории акции
|
||||
- `MoexBondHistoryEntry` — запись истории облигации
|
||||
- `MoexBoard` — торговая доска
|
||||
48
apps/docs/docs/backend/overview.md
Normal file
48
apps/docs/docs/backend/overview.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Backend Overview
|
||||
|
||||
Backend — NestJS-приложение, единственная точка доступа к MOEX ISS.
|
||||
|
||||
## Entry Point
|
||||
|
||||
`apps/backend/src/main.ts` — bootstrap:
|
||||
|
||||
- Глобальный префикс `/api/v1`
|
||||
- Swagger на `/api/docs` (title: "MoexVibe API", version: "1.0.0")
|
||||
- ValidationPipe, HttpExceptionFilter, TransformInterceptor, RequestLoggingMiddleware
|
||||
- CORS включён
|
||||
- Порт из `PORT` env (default: 3000)
|
||||
|
||||
## Root Module
|
||||
|
||||
`apps/backend/src/app.module.ts` импортирует:
|
||||
|
||||
- `ConfigModule.forRoot` (глобальный, из `config/configuration.ts`)
|
||||
- Импортируются 6 feature-модулей (Cache + MoexClient — глобальные)
|
||||
|
||||
## Source Layout
|
||||
|
||||
```
|
||||
apps/backend/src/
|
||||
├── main.ts
|
||||
├── app.module.ts
|
||||
├── config/
|
||||
│ └── configuration.ts
|
||||
├── common/
|
||||
│ ├── dto/
|
||||
│ │ ├── api-response.dto.ts
|
||||
│ │ └── pagination.dto.ts
|
||||
│ ├── filters/
|
||||
│ │ └── http-exception.filter.ts
|
||||
│ ├── interceptors/
|
||||
│ │ └── transform.interceptor.ts
|
||||
│ └── middleware/
|
||||
│ └── request-logging.middleware.ts
|
||||
└── modules/
|
||||
├── moex-client/
|
||||
├── cache/
|
||||
├── health/
|
||||
├── securities/
|
||||
├── shares/
|
||||
├── bonds/
|
||||
└── candles/
|
||||
```
|
||||
30
apps/docs/docs/development/codegen.md
Normal file
30
apps/docs/docs/development/codegen.md
Normal file
@ -0,0 +1,30 @@
|
||||
# Code Generation
|
||||
|
||||
## OpenAPI Types
|
||||
|
||||
Frontend генерирует TypeScript-типы из Swagger-спецификации бэкенда.
|
||||
|
||||
### Generate Types
|
||||
|
||||
```bash
|
||||
npm run codegen -w apps/frontend
|
||||
```
|
||||
|
||||
Выполняет:
|
||||
|
||||
```bash
|
||||
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`
|
||||
|
||||
### Manual Types
|
||||
|
||||
Помимо codegen, используются рукописные типы в `apps/frontend/src/api/responses.ts`. Они не полностью соответствуют codegen'овым и поддерживаются вручную.
|
||||
40
apps/docs/docs/development/commands.md
Normal file
40
apps/docs/docs/development/commands.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Commands
|
||||
|
||||
## Root Workspace
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `npm run dev:backend` | Запуск NestJS в режиме watch на :3000 |
|
||||
| `npm run dev:frontend` | Vite dev-сервер на :5173, проксирует `/api` → :3000 |
|
||||
| `npm run build:backend` | `nest build` |
|
||||
| `npm run build:frontend` | `tsc -b && vite build` (в две фазы) |
|
||||
| `npm run test:backend` | `vitest run` (SWC, не ts-jest) |
|
||||
| `npm run lint` | ESLint только для бэкенда |
|
||||
| `npm run format` | Prettier для всех `*.{ts,tsx}` |
|
||||
| `npm run format:check` | Prettier check для всех `*.{ts,tsx}` |
|
||||
|
||||
## Backend Workspace
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `npm run build -w apps/backend` | `nest build` |
|
||||
| `npm run start:dev -w apps/backend` | `nest start --watch` |
|
||||
| `npm run start:prod -w apps/backend` | `node dist/main` |
|
||||
| `npm run lint -w apps/backend` | ESLint для `{src,test}/**/*.ts` |
|
||||
| `npm run test -w apps/backend` | `vitest run` |
|
||||
| `npm run test:watch -w apps/backend` | `vitest` |
|
||||
|
||||
## Frontend Workspace
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `npm run dev -w apps/frontend` | `vite` |
|
||||
| `npm run build -w apps/frontend` | `tsc -b && vite build` |
|
||||
| `npm run preview -w apps/frontend` | `vite preview` |
|
||||
| `npm run codegen -w apps/frontend` | `openapi-typescript` из Swagger → `src/api/types.ts` |
|
||||
|
||||
## Single Test
|
||||
|
||||
```bash
|
||||
npx vitest run apps/backend/src/modules/shares/shares.service.spec.ts -w apps/backend
|
||||
```
|
||||
57
apps/docs/docs/development/conventions.md
Normal file
57
apps/docs/docs/development/conventions.md
Normal file
@ -0,0 +1,57 @@
|
||||
# Code Conventions
|
||||
|
||||
## Formatting
|
||||
|
||||
Prettier (`.prettierrc`):
|
||||
|
||||
```json
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"semi": true
|
||||
}
|
||||
```
|
||||
|
||||
Проверка: `npm run format:check`
|
||||
Применение: `npm run format`
|
||||
|
||||
## Linting
|
||||
|
||||
ESLint только для бэкенда (`apps/backend`).
|
||||
|
||||
Плагины:
|
||||
- `@typescript-eslint/eslint-plugin`
|
||||
- `@typescript-eslint/parser`
|
||||
|
||||
Запуск: `npm run lint`
|
||||
|
||||
## Naming (backend)
|
||||
|
||||
- ПаскальКейс для модулей, контроллеров, сервисов
|
||||
- `const` для всех переменных (кроме случаев, где нужна мутация)
|
||||
- DTO-файлы в `dto/` внутри каждого модуля
|
||||
|
||||
## Imports (backend)
|
||||
|
||||
Алиас: `@/*` → `apps/backend/src/*`
|
||||
|
||||
## Imports (frontend)
|
||||
|
||||
Алиас: `@/*` → `apps/frontend/src/*`
|
||||
|
||||
## TypeScript
|
||||
|
||||
Настройки из `tsconfig.base.json`:
|
||||
|
||||
- Target: ES2022
|
||||
- Module: ESNext
|
||||
- Module resolution: bundler
|
||||
- Strict: true
|
||||
- ESModule interop: true
|
||||
|
||||
## Backend-specific
|
||||
|
||||
- Использует SWC через `unplugin-swc` (vitest config)
|
||||
- Контроллеры содержат только маршрутизацию и вызовы сервисов
|
||||
- Сервисы содержат бизнес-логику, вызовы MoexClient и кеширование
|
||||
42
apps/docs/docs/development/testing.md
Normal file
42
apps/docs/docs/development/testing.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Testing
|
||||
|
||||
## Backend Tests
|
||||
|
||||
Фреймворк: **vitest** (с `unplugin-swc` для быстрой компиляции, не ts-jest).
|
||||
|
||||
Конфигурация vitest в `apps/backend/` использует SWC для трансформации TypeScript.
|
||||
|
||||
### Run All Tests
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
npm run test:watch -w apps/backend
|
||||
```
|
||||
|
||||
## Test Files
|
||||
|
||||
| File | What it tests |
|
||||
|---|---|
|
||||
| `shares.service.spec.ts` | SharesService |
|
||||
| `bonds.service.spec.ts` | BondsService |
|
||||
| `candles.service.spec.ts` | CandlesService |
|
||||
| `securities.controller.spec.ts` | SecuritiesController |
|
||||
| `securities.service.spec.ts` | SecuritiesService |
|
||||
| `moex-client.service.spec.ts` | MoexClientService |
|
||||
|
||||
## Frontend Tests
|
||||
|
||||
Фронтенд-тесты отсутствуют.
|
||||
45
apps/docs/docs/frontend/api-client.md
Normal file
45
apps/docs/docs/frontend/api-client.md
Normal file
@ -0,0 +1,45 @@
|
||||
# API Client
|
||||
|
||||
## Client (`api/client.ts`)
|
||||
|
||||
Использует нативный `fetch` с единой обёрткой `request<T>`.
|
||||
|
||||
```typescript
|
||||
async function request<T>(
|
||||
path: string,
|
||||
params?: Record<string, string>,
|
||||
): Promise<{ data: T; meta: ApiResponseMeta }>
|
||||
```
|
||||
|
||||
- Формирует URL из `path` + query params
|
||||
- Парсит JSON-ответ в `ApiEnvelope<{ data: T, meta: ApiResponseMeta }>`
|
||||
- Выбрасывает `Error` при HTTP-ошибке
|
||||
|
||||
## API Functions
|
||||
|
||||
| Function | Method | Path |
|
||||
|---|---|---|
|
||||
| `getHealth()` | GET | `/api/v1/health` |
|
||||
| `searchSecurities(q, type?, limit?)` | GET | `/api/v1/securities/search` |
|
||||
| `getShare(secid)` | GET | `/api/v1/securities/shares/:secid` |
|
||||
| `getShareMarketData(secid)` | GET | `/api/v1/securities/shares/:secid/marketdata` |
|
||||
| `getShareDividends(secid)` | GET | `/api/v1/securities/shares/:secid/dividends` |
|
||||
| `getShareHistory(secid, from, till)` | GET | `/api/v1/securities/shares/:secid/history` |
|
||||
| `getBond(secid)` | GET | `/api/v1/securities/bonds/:secid` |
|
||||
| `getBondMarketData(secid)` | GET | `/api/v1/securities/bonds/:secid/marketdata` |
|
||||
| `getBondHistory(secid, from, till)` | GET | `/api/v1/securities/bonds/:secid/history` |
|
||||
| `getShareCandles(secid, interval, from, till)` | GET | `/api/v1/securities/shares/:secid/candles` |
|
||||
| `getBondCandles(secid, interval, from, till)` | GET | `/api/v1/securities/bonds/:secid/candles` |
|
||||
|
||||
## Types
|
||||
|
||||
Ручные типы ответов в `api/responses.ts`:
|
||||
|
||||
- `ApiResponseMeta` — `{ cachedAt, fromCache }`
|
||||
- `ApiEnvelope<T>` — `{ data: T, meta: ApiResponseMeta }`
|
||||
- `ShareResponse` — спецификация акции + `StockMarketData`
|
||||
- `BondResponse` — спецификация облигации + `BondMarketData`
|
||||
- `CandleItem`, `DividendItem`, `ShareHistoryItem`, `BondHistoryItem`
|
||||
- `SearchResultItem`, `HealthResponse`
|
||||
|
||||
Codegen-типы из OpenAPI в `api/types.ts` (генерируются через `npm run codegen`).
|
||||
40
apps/docs/docs/frontend/components.md
Normal file
40
apps/docs/docs/frontend/components.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Components
|
||||
|
||||
## Layout (`components/Layout.tsx`)
|
||||
|
||||
Базовый layout с шапкой и `<Outlet />`.
|
||||
|
||||
- Шапка: логотип "MoexVibe" (ссылка на `/`) + `SearchBar`
|
||||
- Основной контент: max-width 1200px, padding 24px
|
||||
|
||||
## SearchBar (`components/SearchBar.tsx`)
|
||||
|
||||
Поиск инструментов с debounce (300ms).
|
||||
|
||||
- Вызывает `useSearch(debounced)` при вводе 2+ символов
|
||||
- Показывает выпадающий список результатов
|
||||
- При клике на результат переходит на `/stocks/:secid` или `/bonds/:secid`
|
||||
- Закрывается при клике вне компонента
|
||||
|
||||
## PriceChart (`components/PriceChart.tsx`)
|
||||
|
||||
График цены на основе `lightweight-charts` v4.
|
||||
|
||||
- Принимает массив свечей: `{ open, high, low, close, begin }`
|
||||
- Высота по умолчанию: 400px
|
||||
- Цвета: зелёный для роста, красный для падения
|
||||
- Адаптивная ширина (resize listener)
|
||||
|
||||
## StockDetails (`components/StockDetails.tsx`)
|
||||
|
||||
Карточка с информацией об акции.
|
||||
|
||||
- Принимает `ShareResponse`
|
||||
- Отображает: название, тикер, ISIN, цена, изменение (%), open/high/low, объём, капитализация, уровень листинга
|
||||
|
||||
## BondDetails (`components/BondDetails.tsx`)
|
||||
|
||||
Карточка с информацией об облигации.
|
||||
|
||||
- Принимает `BondResponse`
|
||||
- Отображает: название, ISIN, цена (в % от номинала), номинал, дата погашения, купон (сумма/%), период купона, НКД, доходность к погашению, дюрация, тип
|
||||
47
apps/docs/docs/frontend/hooks.md
Normal file
47
apps/docs/docs/frontend/hooks.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Hooks
|
||||
|
||||
Все хуки используют TanStack Query v5.
|
||||
|
||||
| Hook | Query Key | Stale Time | Description |
|
||||
|---|---|---|---|
|
||||
| `useSearch(query)` | `['securities', 'search', query]` | 60s | Поиск инструментов (enabled: query ≥ 2 символов) |
|
||||
| `useStock(secid)` | `['stock', secid]` | 900s | Спецификация акции |
|
||||
| `useStockCandles(secid, interval, from, till)` | `['stockCandles', secid, interval, from, till]` | 3600s | Свечи акции |
|
||||
| `useStockDividends(secid)` | `['stockDividends', secid]` | 86400s | Дивиденды акции |
|
||||
| `useBond(secid)` | `['bond', secid]` | 900s | Спецификация облигации |
|
||||
| `useBondCandles(secid, interval, from, till)` | `['bondCandles', secid, interval, from, till]` | 3600s | Свечи облигации |
|
||||
|
||||
## Query Configuration
|
||||
|
||||
```typescript
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 900_000, // 15 min
|
||||
retry: 2,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Hook Pattern
|
||||
|
||||
Каждый хук:
|
||||
|
||||
1. Вызывает функцию из `api/client.ts`
|
||||
2. Извлекает `res.data` (ответ MOEX обёрнут в `{ data, meta }`)
|
||||
3. Типизирован через рукописные типы из `api/responses.ts`
|
||||
|
||||
```typescript
|
||||
export function useStock(secid: string) {
|
||||
return useQuery<ShareResponse>({
|
||||
queryKey: ['stock', secid],
|
||||
queryFn: async () => {
|
||||
const res = await getShare(secid);
|
||||
return res.data;
|
||||
},
|
||||
staleTime: 900_000,
|
||||
});
|
||||
}
|
||||
```
|
||||
47
apps/docs/docs/frontend/overview.md
Normal file
47
apps/docs/docs/frontend/overview.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Frontend Overview
|
||||
|
||||
React SPA, собранная с Vite.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- React 18
|
||||
- react-router-dom v6
|
||||
- TanStack Query v5
|
||||
- lightweight-charts v4
|
||||
- openapi-fetch (с рукописными типами `responses.ts`)
|
||||
- Vite 5
|
||||
|
||||
## Source Layout
|
||||
|
||||
```
|
||||
apps/frontend/src/
|
||||
├── main.tsx # Entry point
|
||||
├── App.tsx # BrowserRouter
|
||||
├── routes.tsx # Маршруты
|
||||
├── api/
|
||||
│ ├── client.ts # HTTP-клиент (fetch)
|
||||
│ ├── responses.ts # Типы ответов (ручные)
|
||||
│ └── types.ts # Типы из openapi-typescript
|
||||
├── hooks/
|
||||
│ ├── useSearch.ts
|
||||
│ ├── useStock.ts
|
||||
│ ├── useStockCandles.ts
|
||||
│ ├── useStockDividends.ts
|
||||
│ ├── useBond.ts
|
||||
│ └── useBondCandles.ts
|
||||
├── components/
|
||||
│ ├── Layout.tsx
|
||||
│ ├── SearchBar.tsx
|
||||
│ ├── PriceChart.tsx
|
||||
│ ├── StockDetails.tsx
|
||||
│ └── BondDetails.tsx
|
||||
├── pages/
|
||||
│ ├── HomePage.tsx
|
||||
│ ├── StockPage.tsx
|
||||
│ └── BondPage.tsx
|
||||
└── styles.css
|
||||
```
|
||||
|
||||
## Entry Point
|
||||
|
||||
`main.tsx` рендерит `App.tsx`, который содержит `BrowserRouter` → `AppRoutes`.
|
||||
27
apps/docs/docs/frontend/routes.md
Normal file
27
apps/docs/docs/frontend/routes.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Routes
|
||||
|
||||
Определены в `apps/frontend/src/routes.tsx`.
|
||||
|
||||
| Path | Component | Description |
|
||||
|---|---|---|
|
||||
| `/` | `HomePage` | Главная страница с приветствием |
|
||||
| `/stocks/:secid` | `StockPage` | Страница акции |
|
||||
| `/bonds/:secid` | `BondPage` | Страница облигации |
|
||||
|
||||
Все страницы обёрнуты в `Layout`, который содержит:
|
||||
|
||||
- Шапку с логотипом (ссылка на `/`) и `SearchBar`
|
||||
- `<main>` с максимальной шириной 1200px
|
||||
- `<Outlet />` для контента страницы
|
||||
|
||||
## Route Structure
|
||||
|
||||
```tsx
|
||||
<Routes>
|
||||
<Route element={<Layout />}>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/stocks/:secid" element={<StockPage />} />
|
||||
<Route path="/bonds/:secid" element={<BondPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
17
apps/docs/docs/frontend/styling.md
Normal file
17
apps/docs/docs/frontend/styling.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Styling
|
||||
|
||||
## Approach
|
||||
|
||||
CSS через единый `styles.css` с CSS custom properties. Без CSS-in-JS или Tailwind.
|
||||
|
||||
## CSS Custom Properties
|
||||
|
||||
Определены в `styles.css`:
|
||||
|
||||
- `--color-surface` — фон карточек
|
||||
- `--color-text` — основной цвет текста
|
||||
- `--color-text-secondary` — второстепенный цвет текста
|
||||
- `--color-positive` — положительное изменение (#2e7d32)
|
||||
- `--color-negative` — отрицательное изменение (#c62828)
|
||||
- `--border-radius` — радиус скругления
|
||||
- `--shadow` — тень для карточек
|
||||
43
apps/docs/docs/getting-started.md
Normal file
43
apps/docs/docs/getting-started.md
Normal file
@ -0,0 +1,43 @@
|
||||
# Getting Started
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 20+
|
||||
- npm 9+
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Clone repository
|
||||
git clone <repo-url>
|
||||
cd moex-vibe
|
||||
|
||||
# 2. Install dependencies
|
||||
npm install
|
||||
|
||||
# 3. Start backend (http://localhost:3000)
|
||||
npm run dev:backend
|
||||
|
||||
# 4. In another terminal — start frontend (http://localhost:5173)
|
||||
npm run dev:frontend
|
||||
```
|
||||
|
||||
Frontend dev-сервер проксирует `/api` → `http://localhost:3000`.
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
- Frontend: http://localhost:80
|
||||
- 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:
|
||||
|
||||
1. Страница с поисковой строкой и заголовком "MoexVibe"
|
||||
2. Swagger UI на http://localhost:3000/api/docs со всеми endpoint'ами
|
||||
3. `curl http://localhost:3000/api/v1/health` возвращает `{ "status": "ok", ... }`
|
||||
42
apps/docs/docs/infrastructure/ci.md
Normal file
42
apps/docs/docs/infrastructure/ci.md
Normal file
@ -0,0 +1,42 @@
|
||||
# CI/CD
|
||||
|
||||
## CI Pipeline
|
||||
|
||||
Определён в `.gitea/workflows/ci.yml`. Запускается на push/PR в `main`.
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
NODE_VERSION: 20
|
||||
```
|
||||
|
||||
## Jobs
|
||||
|
||||
### lint
|
||||
|
||||
- `actions/checkout@v4`
|
||||
- `actions/setup-node@v4` (Node 20)
|
||||
- `npm ci`
|
||||
- `npm run lint` (ESLint для бэкенда)
|
||||
- `npx prettier --check "**/*.{ts,tsx}"` (проверка форматирования)
|
||||
|
||||
### test
|
||||
|
||||
- `actions/checkout@v4`
|
||||
- `actions/setup-node@v4` (Node 20)
|
||||
- `npm ci`
|
||||
- `npm run test:backend` (vitest)
|
||||
|
||||
### build
|
||||
|
||||
- `actions/checkout@v4`
|
||||
- `actions/setup-node@v4` (Node 20)
|
||||
- `npm ci`
|
||||
- `npm run build:backend` (nest build)
|
||||
- `npm run build:frontend` (tsc -b && vite build)
|
||||
112
apps/docs/docs/infrastructure/docker.md
Normal file
112
apps/docs/docs/infrastructure/docker.md
Normal file
@ -0,0 +1,112 @@
|
||||
# Docker
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
User["User"]
|
||||
Nginx["Nginx :80"]
|
||||
Backend["Node.js :3000"]
|
||||
MOEX["MOEX ISS<br/>iss.moex.com"]
|
||||
|
||||
User -->|"HTTP"| Nginx
|
||||
Nginx -->|"/api/* proxy_pass"| Backend
|
||||
Nginx -->|"/* static files"| Nginx
|
||||
Backend -->|"API calls"| MOEX
|
||||
```
|
||||
|
||||
## Services
|
||||
|
||||
### Backend (`Dockerfile.backend`)
|
||||
|
||||
```dockerfile
|
||||
FROM node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY package.json tsconfig.base.json ./
|
||||
COPY apps/backend/ apps/backend/
|
||||
RUN npm install
|
||||
RUN npm run build -w apps/backend
|
||||
|
||||
FROM node:20-alpine AS production
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/apps/backend/dist ./dist
|
||||
COPY --from=build /app/apps/backend/node_modules ./node_modules
|
||||
COPY --from=build /app/apps/backend/package.json ./
|
||||
EXPOSE 3000
|
||||
CMD ["node", "dist/main.js"]
|
||||
```
|
||||
|
||||
Multi-stage build: сборка → production-образ с минимальными слоями.
|
||||
|
||||
### Frontend (`Dockerfile.frontend`)
|
||||
|
||||
```dockerfile
|
||||
FROM node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY package.json tsconfig.base.json ./
|
||||
COPY apps/frontend/ apps/frontend/
|
||||
RUN npm install
|
||||
RUN npm run build -w apps/frontend
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY --from=build /app/apps/frontend/dist /usr/share/nginx/html
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
```
|
||||
|
||||
Собирает статику, затем раздаёт через Nginx.
|
||||
|
||||
### Nginx Config (`nginx.conf`)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `/api/*` проксируется на backend-сервис
|
||||
- Все остальные пути отдают SPA (index.html для клиентских маршрутов)
|
||||
|
||||
## Docker Compose (`docker-compose.yml`)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile.backend
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- PORT=3000
|
||||
- MOEX_BASE_URL=https://iss.moex.com/iss
|
||||
- MOEX_RATE_LIMIT=10
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile.frontend
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- backend
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
36
apps/docs/docs/intro.md
Normal file
36
apps/docs/docs/intro.md
Normal file
@ -0,0 +1,36 @@
|
||||
# MoexVibe
|
||||
|
||||
Веб-приложение для анализа ценных бумаг Московской биржи (MOEX).
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend:** NestJS, TypeScript, OpenAPI (Swagger)
|
||||
- **Frontend:** React 18, TypeScript, Vite, TanStack Query v5, lightweight-charts v4
|
||||
- **Infrastructure:** Docker, docker-compose
|
||||
- **CI:** Gitea Actions
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
moex-vibe/
|
||||
├── apps/
|
||||
│ ├── backend/ # NestJS API (единственная точка доступа к MOEX)
|
||||
│ └── frontend/ # React SPA
|
||||
├── docs/
|
||||
│ ├── architecture/ # ADR и диаграммы
|
||||
│ ├── openapi/ # OpenAPI-спецификация
|
||||
│ └── superpowers/ # Дизайн-спеки и планы
|
||||
├── docker/
|
||||
│ ├── Dockerfile.backend
|
||||
│ ├── Dockerfile.frontend
|
||||
│ └── nginx.conf
|
||||
├── docker-compose.yml
|
||||
└── tsconfig.base.json
|
||||
```
|
||||
|
||||
## Key Principles
|
||||
|
||||
- Backend — единственный клиент MOEX. Frontend никогда не обращается к MOEX напрямую.
|
||||
- npm workspaces монорепозиторий: `apps/backend` и `apps/frontend`.
|
||||
- Глобальный префикс API: `/api/v1`. Swagger: `/api/docs`.
|
||||
- Ответы API обёрнуты в `{ data: T, meta: { fromCache, cachedAt } }`.
|
||||
65
apps/docs/docusaurus.config.ts
Normal file
65
apps/docs/docusaurus.config.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { themes as prismThemes } from 'prism-react-renderer';
|
||||
import type { Config } from '@docusaurus/types';
|
||||
import type * as Preset from '@docusaurus/preset-classic';
|
||||
|
||||
const config: Config = {
|
||||
title: 'MoexVibe',
|
||||
tagline: 'Analysis of MOEX securities',
|
||||
favicon: 'img/favicon.ico',
|
||||
|
||||
url: 'https://moexvibe.example.com',
|
||||
baseUrl: '/',
|
||||
|
||||
onBrokenLinks: 'warn',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
|
||||
i18n: {
|
||||
defaultLocale: 'ru',
|
||||
locales: ['ru'],
|
||||
},
|
||||
|
||||
presets: [
|
||||
[
|
||||
'classic',
|
||||
{
|
||||
docs: {
|
||||
routeBasePath: '/',
|
||||
sidebarPath: './sidebars.ts',
|
||||
editUrl: undefined,
|
||||
},
|
||||
theme: {
|
||||
customCss: './src/css/custom.css',
|
||||
},
|
||||
} satisfies Preset.Options,
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
navbar: {
|
||||
title: 'MoexVibe',
|
||||
items: [
|
||||
{
|
||||
type: 'docSidebar',
|
||||
sidebarId: 'docsSidebar',
|
||||
position: 'left',
|
||||
label: 'Документация',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/ksv741/moex-vibe',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: 'dark',
|
||||
copyright: `Copyright © ${new Date().getFullYear()} MoexVibe`,
|
||||
},
|
||||
prism: {
|
||||
theme: prismThemes.github,
|
||||
darkTheme: prismThemes.dracula,
|
||||
},
|
||||
} satisfies Preset.ThemeConfig,
|
||||
};
|
||||
|
||||
export default config;
|
||||
21
apps/docs/package.json
Normal file
21
apps/docs/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@moex-vibe/docs",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"serve": "docusaurus serve"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.7.0",
|
||||
"@docusaurus/preset-classic": "3.7.0",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.0",
|
||||
"typescript": "^5.3.0"
|
||||
}
|
||||
}
|
||||
64
apps/docs/sidebars.ts
Normal file
64
apps/docs/sidebars.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import type { SidebarsConfig } from '@docusaurus/plugin-content-docs';
|
||||
|
||||
const sidebars: SidebarsConfig = {
|
||||
docsSidebar: [
|
||||
'intro',
|
||||
'getting-started',
|
||||
'architecture',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Backend',
|
||||
items: [
|
||||
'backend/overview',
|
||||
'backend/modules',
|
||||
'backend/api',
|
||||
'backend/configuration',
|
||||
'backend/caching',
|
||||
'backend/moex-client',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Frontend',
|
||||
items: [
|
||||
'frontend/overview',
|
||||
'frontend/routes',
|
||||
'frontend/components',
|
||||
'frontend/hooks',
|
||||
'frontend/api-client',
|
||||
'frontend/styling',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Infrastructure',
|
||||
items: ['infrastructure/docker', 'infrastructure/ci'],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Development',
|
||||
items: [
|
||||
'development/commands',
|
||||
'development/testing',
|
||||
'development/codegen',
|
||||
'development/conventions',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Architecture Decisions (ADR)',
|
||||
items: [
|
||||
'adr/index',
|
||||
'adr/ADR-001-backend-single-point-of-access',
|
||||
'adr/ADR-002-in-memory-cache',
|
||||
'adr/ADR-003-rate-limiting-strategy',
|
||||
'adr/ADR-004-feature-modules',
|
||||
'adr/ADR-005-openapi-codegen-frontend',
|
||||
'adr/ADR-006-no-cci',
|
||||
'adr/ADR-007-two-level-caching',
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default sidebars;
|
||||
13
apps/docs/src/css/custom.css
Normal file
13
apps/docs/src/css/custom.css
Normal file
@ -0,0 +1,13 @@
|
||||
:root {
|
||||
--ifm-color-primary: #1976d2;
|
||||
--ifm-color-primary-dark: #1565c0;
|
||||
--ifm-color-primary-darker: #0d47a1;
|
||||
--ifm-color-primary-light: #2196f3;
|
||||
--ifm-color-primary-lighter: #42a5f5;
|
||||
--ifm-code-font-size: 95%;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--docusaurus-highlighted-code-line-bg: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
47
docs/superpowers/specs/2026-06-13-docusaurus-docs-design.md
Normal file
47
docs/superpowers/specs/2026-06-13-docusaurus-docs-design.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Docusaurus Documentation — Design Spec
|
||||
|
||||
**Date:** 2026-06-13
|
||||
|
||||
## Purpose
|
||||
|
||||
Создать Docusaurus-сайт для onboard'инга новых разработчиков за 15 минут и документирования архитектуры MoexVibe.
|
||||
|
||||
## Location
|
||||
|
||||
`apps/docs/` — npm workspace в монорепозитории (по аналогии с `apps/backend`, `apps/frontend`).
|
||||
|
||||
## Content Sources (только из кода, без вымысла)
|
||||
|
||||
- `package.json` корневой + воркспейсов → scripts, deps
|
||||
- `AGENTS.md` → env vars, architecture description, commands table
|
||||
- `apps/backend/src/**` → модули, контроллеры, сервисы, DTO, конфиг
|
||||
- `apps/frontend/src/**` → компоненты, хуки, клиент, типы
|
||||
- `docker/Dockerfile.*`, `docker/nginx.conf` → инфраструктура
|
||||
- `docker-compose.yml` → сервисы, порты
|
||||
- `.gitea/workflows/ci.yml` → CI pipeline
|
||||
- `docs/architecture/adr/*` → копия существующих ADR
|
||||
- `.prettierrc`, `tsconfig.base.json` → конфиги
|
||||
|
||||
## Pages
|
||||
|
||||
| Раздел | Страницы |
|
||||
|---|---|
|
||||
| Overview | intro, getting-started, architecture |
|
||||
| Backend | overview, modules, api, configuration, caching, moex-client |
|
||||
| Frontend | overview, routes, components, hooks, api-client, styling |
|
||||
| Infrastructure | docker, ci |
|
||||
| Development | commands, testing, codegen, conventions |
|
||||
| ADR | index + 7 ADR-документов (копия) |
|
||||
|
||||
## Mermaid Diagrams
|
||||
|
||||
1. **architecture** — `graph TD` (Browser → SPA → Backend → Cache → MOEX)
|
||||
2. **architecture** — `sequenceDiagram` (cache hit/miss flow)
|
||||
3. **backend/modules** — `graph TD` (module dependency graph)
|
||||
4. **backend/caching** — `flowchart LR` (getOrFetch lifecycle)
|
||||
5. **infrastructure/docker** — `graph TD` (deployment)
|
||||
|
||||
## Root Changes
|
||||
|
||||
- Добавить `apps/docs` в `workspaces[]` корневого `package.json`
|
||||
- Добавить скрипты `dev:docs` и `build:docs`
|
||||
15480
package-lock.json
generated
15480
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,8 @@
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"apps/backend",
|
||||
"apps/frontend"
|
||||
"apps/frontend",
|
||||
"apps/docs"
|
||||
],
|
||||
"scripts": {
|
||||
"dev:backend": "npm run start:dev -w apps/backend",
|
||||
@ -13,7 +14,9 @@
|
||||
"test:backend": "npm run test -w apps/backend",
|
||||
"lint": "npm run lint -w apps/backend",
|
||||
"format": "prettier --write \"**/*.{ts,tsx}\"",
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx}\""
|
||||
"format:check": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"dev:docs": "npm run dev -w apps/docs",
|
||||
"build:docs": "npm run build -w apps/docs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.0.0"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user