Files
Randify.pro/.sisyphus/plans/dm-dashboard.md
T
emil aaf596a7bc fix(dm): OAuth redirect_uri, orange buttons, denser layout, hide lang switcher
- Fix OAuth redirect_uri mismatch in VK/Yandex callbacks (use PUBLIC_APP_URL)
- Make quick dice buttons and anchor nav links orange by default
- Densify DM Dashboard layout (smaller gaps, tighter spacing)
- Hide LanguageSwitcher/Blog/About/Privacy links on DM pages
- Increase notes textarea height for better usability
2026-05-15 01:42:09 +03:00

1311 lines
53 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DM Dashboard MVP — Рабочий план
## TL;DR
> **Quick Summary**: Создать MVP DM Dashboard (`/dm/*`) внутри Randify.pro — инструмент для мастеров D&D 5e с кубиками, трекером инициативы, справочником Open5e, локальными заметками и базовой OAuth-аутентификацией (VK + Yandex).
>
> **Deliverables**:
> - Гибридный Astro билд с API routes
> - PostgreSQL схема + Drizzle ORM + миграции
> - OAuth 2.1 + PKCE (VK + Yandex)
> - JWT сессии в httpOnly cookies
> - Dice Roller (клиент-side)
> - Initiative Tracker (простой список)
> - Open5e Reference (поиск + карточки)
> - Notes (localStorage)
> - Оранжевая тема оформления
> - TDD инфраструктура + тесты
>
> **Estimated Effort**: Large
> **Parallel Execution**: YES — 3 волны + финальная верификация
> **Critical Path**: T1 (Hybrid Config) → T2 (DB Schema) → T7 (OAuth API) → T14 (DM Layout) → F1-F4
---
## Context
### Original Request
DM Dashboard — инструмент для мастеров настольных RPG, живёт на субдомене dm.randify.pro как часть проекта randify.pro. Три уровня доступа (FREE без аккаунта, FREE с аккаунтом, PRO), OAuth (VK/Yandex), Open5e справочник, AI-генерация, Boosty подписки.
### Interview Summary
**Key Discussions**:
- **MVP vs Iteration 2**: MVP — базовые инструменты без AI. Итерация 2 — AI, PRO, Boosty, закладки, энкаунтер-билдер.
- **Архитектура**: Переход с static Astro на hybrid (API routes + Server Islands).
- **БД**: PostgreSQL на reg.ru, Drizzle ORM.
- **Аутентификация**: OAuth 2.1 + PKCE, JWT в httpOnly cookies.
- **Дизайн**: Оранжевая тема в стиле ролёвок, похожая на текущий Randify.pro.
- **i18n**: Только RU для DM Dashboard.
- **Тесты**: TDD с Vitest.
**Research Findings**:
- Randify — чисто статический сайт (27 генераторов), требуется серьёзная архитектурная трансформация.
- Open5e V2 API с field filtering и pagination.
- VK и Yandex требуют OAuth 2.1 + PKCE.
- Token bucket — стандарт для rate limiting (пригодится в итерации 2).
- Текущий деплой: Docker multi-stage (nginx:alpine) → нужен Node.js runtime.
### Metis Review
**Identified Gaps** (addressed):
- Core Objective разделён на MVP и Product Vision.
- Scope IN/OUT чётко определены.
- Противоречия (static→hybrid, RU-only) задокументированы как осознанные отклонения.
- Тестовая стратегия (TDD) определена.
---
## Work Objectives
### Core Objective
Создать MVP DM Dashboard — модуль `/dm/*` внутри Randify.pro с базовыми инструментами для мастеров D&D 5e: кубиками, трекером инициативы, справочником Open5e, локальными заметками и базовой OAuth-аутентификацией (VK + Yandex).
### Concrete Deliverables
- `astro.config.mjs` — обновлённая конфигурация hybrid mode
- `Dockerfile` + `docker-compose.yml` — Node.js runtime + PostgreSQL
- `src/db/schema.ts` — Drizzle ORM схема
- `src/db/migrate.ts` — скрипт миграций
- `src/pages/api/auth/[...path].ts` — API routes для OAuth
- `src/lib/auth/jwt.ts` — JWT утилиты
- `src/lib/auth/oauth.ts` — OAuth PKCE утилиты
- `src/pages/dm/index.astro` — главная страница DM Dashboard
- `src/layouts/DmLayout.astro` — лейаут DM Dashboard
- `src/components/dm/DiceRoller.astro` — кубики
- `src/components/dm/InitiativeTracker.astro` — инициатива
- `src/components/dm/Open5eReference.astro` — справочник
- `src/components/dm/NotesPanel.astro` — заметки
- `src/lib/open5e/client.ts` — клиент Open5e API
- `src/lib/open5e/cache.ts` — кэширование
- `src/i18n/dm-translations.ts` — переводы DM Dashboard (RU)
- `tests/` — TDD тесты для всех модулей
### Definition of Done
- [ ] `npm run build` проходит без ошибок
- [ ] `npm run test` — все тесты проходят
- [ ] `docker compose up` запускает приложение + PostgreSQL
- [ ] OAuth вход через VK работает
- [ ] OAuth вход через Yandex работает
- [ ] Кубики генерируют корректные значения
- [ ] Инициатива сортирует по убыванию
- [ ] Open5e загружает и ищет монстров
- [ ] Заметки сохраняются в localStorage
- [ ] Дизайн в оранжевой теме
### Must Have
- [ ] Гибридный Astro билд
- [ ] PostgreSQL подключение
- [ ] OAuth аутентификация (VK + Yandex)
- [ ] JWT сессии
- [ ] Dice Roller
- [ ] Initiative Tracker
- [ ] Open5e Reference (поиск + просмотр)
- [ ] Notes (localStorage)
- [ ] Оранжевая тема
- [ ] RU i18n
- [ ] TDD тесты
### Must NOT Have (Guardrails)
- [ ] AI-генерация любого контента (Итерация 2)
- [ ] Rate limiting (нужен AI)
- [ ] PRO-tier логика (Итерация 2)
- [ ] Boosty верификация (Итерация 2)
- [ ] Закладки Open5e (Итерация 2)
- [ ] Энкаунтер-билдер (Итерация 2)
- [ ] Сохранение в PostgreSQL для пользовательских данных (Итерация 2)
- [ ] EN-версия DM Dashboard (Итерация 2)
- [ ] PWA / Service Worker (Итерация 2)
- [ ] Изменения существующих генераторов
- [ ] Удаление существующих страниц
---
## Verification Strategy
> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed. No exceptions.
### Test Decision
- **Infrastructure exists**: YES (Vitest в проекте)
- **Automated tests**: TDD
- **Framework**: Vitest
- **TDD Workflow**: Каждая задача начинается с RED (падающий тест) → GREEN (минимальная реализация) → REFACTOR
### QA Policy
Every task MUST include agent-executed QA scenarios.
Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`.
- **Frontend/UI**: Playwright — Navigate, interact, assert DOM, screenshot
- **API/Backend**: Bash (curl) — Send requests, assert status + response fields
- **Database**: Bash (psql/drizzle) — Query tables, assert rows
---
## Execution Strategy
### Parallel Execution Waves
```
Wave 1 (Foundation — start immediately):
├── T1: Astro Hybrid Config + Docker Update
├── T2: PostgreSQL Schema + Drizzle ORM
├── T3: Testing Infrastructure (TDD setup)
├── T4: Design System / Orange Theme
└── T5: OAuth App Setup Documentation
Wave 2 (Core Features — MAX PARALLEL):
├── T6: OAuth PKCE + API Routes (depends: T1, T2)
├── T7: JWT Sessions + Middleware (depends: T1, T2)
├── T8: Dice Roller Component (depends: T4)
├── T9: Initiative Tracker Component (depends: T4)
├── T10: Open5e API Client + Cache (depends: T1)
├── T11: Open5e Search UI (depends: T4, T10)
├── T12: Notes Component (depends: T4)
└── T13: Auth UI (Login/Logout/Profile) (depends: T6, T7)
Wave 3 (Integration + Polish):
├── T14: DM Dashboard Layout + Page (depends: T4, T6, T7, T8, T9, T10, T11, T12, T13)
├── T15: Navigation + Routing (depends: T14)
├── T16: Build Verification + CI Update (depends: T1, T14)
└── T17: Final Styling Polish (depends: T14, T15)
Wave FINAL (4 parallel reviews → user okay):
├── F1: Plan Compliance Audit (oracle)
├── F2: Code Quality Review (unspecified-high)
├── F3: Real QA (unspecified-high + playwright)
└── F4: Scope Fidelity Check (deep)
```
### Dependency Matrix
- **T1**: - → T6, T7, T10, T14, T16
- **T2**: - → T6, T7, T16
- **T3**: - → T8, T9, T10, T11, T12
- **T4**: - → T8, T9, T11, T12, T13, T14, T17
- **T5**: - → (документация для пользователя)
- **T6**: T1, T2 → T13, T14
- **T7**: T1, T2 → T13, T14
- **T8**: T4 → T14
- **T9**: T4 → T14
- **T10**: T1 → T11, T14
- **T11**: T4, T10 → T14
- **T12**: T4 → T14
- **T13**: T6, T7, T4 → T14
- **T14**: T4, T6, T7, T8, T9, T10, T11, T12, T13 → T15, T16, T17
- **T15**: T14 → -
- **T16**: T1, T14 → -
- **T17**: T14, T15 → -
### Agent Dispatch Summary
- **Wave 1**: T1→`quick`, T2→`quick`, T3→`quick`, T4→`visual-engineering`, T5→`writing`
- **Wave 2**: T6→`unspecified-high`, T7→`unspecified-high`, T8→`quick`, T9→`quick`, T10→`quick`, T11→`visual-engineering`, T12→`quick`, T13→`visual-engineering`
- **Wave 3**: T14→`deep`, T15→`quick`, T16→`unspecified-high`, T17→`visual-engineering`
- **FINAL**: F1→`oracle`, F2→`unspecified-high`, F3→`unspecified-high`, F4→`deep`
---
## TODOs
- [x] T1. Astro Hybrid Config + Docker Update
**What to do**:
- Обновить `astro.config.mjs` с `output: 'hybrid'` и `adapter: node()`
- Добавить `@astrojs/node` адаптер
- Обновить `Dockerfile`: заменить `nginx:alpine` на `node:20-alpine` с `node ./dist/server/entry.mjs`
- Добавить `docker-compose.yml` с PostgreSQL сервисом
- Обновить `.github/workflows/deploy.yml` для деплоя Node.js приложения
- Проверить, что существующие `/generators/*` маршруты остаются статическими (`export const prerender = true`)
**Must NOT do**:
- Не удалять существующие генераторы или страницы
- Не менять деплой основного сайта (только добавить Node.js runtime)
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: []
- **Reason**: Конфигурационные изменения, не требует сложной логики
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1
- **Blocks**: T6, T7, T10, T14, T16
- **Blocked By**: None
**References**:
- `astro.config.mjs` — текущая статическая конфигурация
- `Dockerfile` — текущий multi-stage build с nginx
- `.github/workflows/deploy.yml` — текущий деплой через rsync
- Astro docs: `@astrojs/node` adapter configuration
**Acceptance Criteria**:
- [ ] `npm run build` создаёт `dist/server/entry.mjs`
- [ ] `docker build -t dm-dashboard .` собирает образ
- [ ] `docker run -p 4321:4321 dm-dashboard` запускает сервер
- [ ] `curl http://localhost:4321/generators/coin-toss/` возвращает статическую страницу
- [ ] `curl http://localhost:4321/api/health` возвращает 200 (тестовый API route)
**QA Scenarios**:
```
Scenario: Astro hybrid build succeeds
Tool: Bash
Steps:
1. npm run build
2. ls dist/server/entry.mjs
Expected Result: entry.mjs существует
Evidence: .sisyphus/evidence/t1-hybrid-build.log
Scenario: Existing generators still static
Tool: Bash (curl)
Steps:
1. docker run -d -p 4321:4321 dm-dashboard
2. curl -s http://localhost:4321/generators/coin-toss/ | grep -o "Coin Flip"
Expected Result: "Coin Flip" найден в ответе
Evidence: .sisyphus/evidence/t1-static-routes.html
```
**Commit**: YES
- Message: `chore(dm): configure astro hybrid mode + docker`
- Files: `astro.config.mjs`, `Dockerfile`, `docker-compose.yml`, `.github/workflows/deploy.yml`, `package.json`
- [x] T2. PostgreSQL Schema + Drizzle ORM
**What to do**:
- Установить `drizzle-orm` и `drizzle-kit`
- Создать `src/db/schema.ts` с таблицами:
- `users` (id, vkId?, yandexId?, email?, name, avatar, createdAt)
- `sessions` (id, userId, token, expiresAt) — для server-side сессий (MVP)
- Создать `src/db/client.ts` — singleton PostgreSQL client
- Создать `src/db/migrate.ts` — скрипт для запуска миграций
- Добавить `drizzle.config.ts`
- Создать `.env.example` с переменными DATABASE_URL, JWT_SECRET
**Must NOT do**:
- Не создавать таблицы для заметок, НПС, энкаунтеров (Итерация 2)
- Не добавлять rate limiting таблицы (Итерация 2)
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: []
- **Reason**: Схема базы данных, типичная задача
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1
- **Blocks**: T6, T7, T16
- **Blocked By**: None
**References**:
- Drizzle ORM docs: PostgreSQL setup, schema definition, migrations
- `src/lib/generator-schema.ts` — Zod patterns (уже в проекте)
- `src/data/config.ts` — конфигурационные паттерны
**Acceptance Criteria**:
- [ ] `npx drizzle-kit generate` создаёт SQL миграции
- [ ] `npx drizzle-kit migrate` применяет миграции к БД
- [ ] `docker compose exec postgres psql -U dmuser -d dmdashboard -c "\dt"` показывает таблицы users и sessions
**QA Scenarios**:
```
Scenario: Database migrations run successfully
Tool: Bash
Preconditions: docker compose up -d
Steps:
1. npx drizzle-kit generate
2. npx drizzle-kit migrate
3. docker compose exec postgres psql -U dmuser -d dmdashboard -c "SELECT tablename FROM pg_tables WHERE schemaname='public'"
Expected Result: users, sessions в списке таблиц
Evidence: .sisyphus/evidence/t2-db-migrations.log
Scenario: Drizzle client connects
Tool: Bash (node)
Steps:
1. node -e "const { db } = require('./src/db/client.ts'); console.log('Connected')"
Expected Result: "Connected" (или эквивалент для ESM)
Evidence: .sisyphus/evidence/t2-db-connection.log
```
**Commit**: YES
- Message: `chore(db): setup drizzle orm + postgresql schema`
- Files: `src/db/schema.ts`, `src/db/client.ts`, `src/db/migrate.ts`, `drizzle.config.ts`, `.env.example`
- [x] T3. Testing Infrastructure (TDD Setup)
**What to do**:
- Убедиться, что Vitest настроен (уже есть в package.json)
- Создать `vitest.config.ts` с алиасами `@/*` → `src/*`
- Добавить `@testing-library/dom` и `happy-dom` (или `jsdom`) для компонентных тестов
- Создать `tests/setup.ts` — общий setup файл
- Создать `tests/utils.ts` — хелперы для тестов (mock auth, mock db)
- Установить `playwright` для компонентных тестов с рендерингом
- Создать `playwright.config.ts` с базовой конфигурацией
- Написать первый тест-скелетон для проверки инфраструктуры
- Добавить скрипты `test:unit` и `test:ui` в package.json
**Must NOT do**:
- Не писать e2e тесты (только unit + component tests)
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: []
- **Reason**: Настройка инфраструктуры
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1
- **Blocks**: T8, T9, T10, T11, T12 (все компоненты требуют тестов)
- **Blocked By**: None
**References**:
- `package.json` — текущие скрипты test
- `tsconfig.json` — алиасы `@/*`
- Vitest docs: configuration, aliases, DOM environment
**Acceptance Criteria**:
- [ ] `npm run test` запускается без ошибок
- [ ] `npm run test:unit` проходит минимум 1 тест
- [ ] Алиасы `@/*` работают в тестах
**QA Scenarios**:
```
Scenario: Vitest runs with aliases
Tool: Bash
Steps:
1. npm run test:unit
2. grep "PASS\|1 passed" в выводе
Expected Result: Тесты проходят, алиасы @/* резолвятся
Evidence: .sisyphus/evidence/t3-vitest-run.log
```
**Commit**: YES
- Message: `chore(tests): setup vitest + tdd infrastructure`
- Files: `vitest.config.ts`, `tests/setup.ts`, `tests/utils.ts`, `package.json`
- [x] T4. Design System / Orange Theme
**What to do**:
- Создать `src/styles/dm-theme.css` с CSS переменными:
- `--accent: #E87722` (оранжевый)
- `--accent-light: #FF9F43`
- `--accent-dark: #D35400`
- `--bg-primary: #0f0f0f`
- `--bg-secondary: #1a1a1a`
- `--bg-card: #242424`
- Создать `src/components/dm/DmButton.astro` — кнопка в DM стиле
- Создать `src/components/dm/DmCard.astro` — карточка в DM стиле
- Создать `src/components/dm/DmInput.astro` — инпут в DM стиле
- Создать `src/components/dm/DmHeader.astro` — хедер DM Dashboard
- Обновить `DmLayout.astro` (заглушку) с подключением темы
**Must NOT do**:
- Не менять существующие компоненты Randify (GeneratorCard, BaseLayout и т.д.)
- Не добавлять анимации сложнее текущих (popElement)
**Recommended Agent Profile**:
- **Category**: `visual-engineering`
- **Skills**: [`/frontend-ui-ux`]
- **Reason**: UI/UX дизайн, стилизация
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1
- **Blocks**: T8, T9, T11, T12, T13, T14, T17
- **Blocked By**: None
**References**:
- `src/layouts/BaseLayout.astro` — текущая тема `--accent: #534AB7`
- `src/components/GeneratorCard.astro` — паттерн карточек
- `src/components/ResultBox.astro` — паттерн отображения результатов
- Tailwind CSS v4 docs: custom theme, CSS variables
**Acceptance Criteria**:
- [ ] Оранжевый цвет `#E87722` применяется к кнопкам
- [ ] Тёмная тема совместима с текущей
- [ ] Компоненты Button, Card, Input рендерятся
**QA Scenarios**:
```
Scenario: Orange theme renders correctly
Tool: Playwright
Steps:
1. Открыть http://localhost:4321/dm/
2. Сделать скриншот
3. Проверить CSS var --accent = #E87722
Expected Result: Оранжевый акцентный цвет виден на кнопках
Evidence: .sisyphus/evidence/t4-orange-theme.png
```
**Commit**: YES
- Message: `feat(ui): add orange dm theme + base components`
- Files: `src/styles/dm-theme.css`, `src/components/dm/*.astro`
- [x] T5. OAuth App Setup Documentation
**What to do**:
- Создать `docs/OAUTH_SETUP.md` с инструкциями:
- Как зарегистрировать VK ID приложение
- Как зарегистрировать Yandex OAuth приложение
- Какие redirect URI настроить (`http://localhost:4321/api/auth/callback/vk`, `.../yandex`)
- Какие scopes запросить (профиль, email)
- Куда сохранить client_id и client_secret (.env)
- Добавить проверку env vars при старте приложения
**Must NOT do**:
- Не коммитить реальные client_id / client_secret
- Не создавать приложения за пользователя
**Recommended Agent Profile**:
- **Category**: `writing`
- **Skills**: []
- **Reason**: Документация
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1
- **Blocks**: (документация для пользователя, не блокирует код)
- **Blocked By**: None
**References**:
- VK ID docs: https://id.vk.com/about/business/go/docs/ru/vkid/latest/vk-id/connection/start-integration
- Yandex OAuth docs: https://yandex.com/dev/id/doc/en/concepts/ya-oauth-intro
**Acceptance Criteria**:
- [ ] Файл `docs/OAUTH_SETUP.md` создан и читаем
- [ ] `.env.example` содержит VK_CLIENT_ID, VK_CLIENT_SECRET, YANDEX_CLIENT_ID, YANDEX_CLIENT_SECRET
- [ ] Приложение проверяет наличие env vars и выдаёт понятную ошибку если их нет
**QA Scenarios**:
```
Scenario: Missing env vars produce clear error
Tool: Bash
Steps:
1. unset VK_CLIENT_ID
2. npm run dev
Expected Result: Приложение не запускается с сообщением "Missing VK_CLIENT_ID"
Evidence: .sisyphus/evidence/t5-env-check.log
```
**Commit**: YES
- Message: `docs(dm): add oauth setup instructions`
- Files: `docs/OAUTH_SETUP.md`, `.env.example`
- [x] T6. OAuth PKCE + API Routes
**What to do**:
- Реализовать PKCE утилиты: `generateCodeVerifier()`, `generateCodeChallenge()`
- Создать `src/pages/api/auth/login/vk.ts` — начало OAuth flow (генерация PKCE, редирект на VK)
- Создать `src/pages/api/auth/callback/vk.ts` — обработка callback (обмен code на token)
- Создать `src/pages/api/auth/login/yandex.ts` — аналогично для Yandex
- Создать `src/pages/api/auth/callback/yandex.ts` — callback для Yandex
- Создать `src/pages/api/auth/logout.ts` — выход
- В `src/lib/auth/oauth.ts` реализовать функции для каждого провайдера
- Сохранение PKCE verifier в cookie (временное, httpOnly)
**Must NOT do**:
- Не использовать localStorage для code_verifier (XSS-риск)
- Не пропускать проверку state parameter
**Recommended Agent Profile**:
- **Category**: `unspecified-high`
- **Skills**: []
- **Reason**: OAuth + PKCE — security-critical
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 2
- **Blocks**: T13, T14
- **Blocked By**: T1, T2
**References**:
- PKCE implementation pattern из research:
```typescript
function generateCodeVerifier(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
```
- VK OAuth docs: https://id.vk.com/about/business/go/docs/ru/vkid/latest/vk-id/connection/realization
- Yandex OAuth docs: https://yandex.com/dev/id/doc/en/concepts/ya-oauth-intro
**Acceptance Criteria**:
- [ ] `curl /api/auth/login/vk` возвращает 302 редирект на VK OAuth
- [ ] Callback route принимает code и state
- [ ] Logout route очищает cookies
**QA Scenarios**:
```
Scenario: VK OAuth login redirects correctly
Tool: Bash (curl)
Steps:
1. curl -s -o /dev/null -w "%{http_code}" http://localhost:4321/api/auth/login/vk
Expected Result: 302 redirect
Evidence: .sisyphus/evidence/t6-vk-login.log
Scenario: Invalid state rejected
Tool: Bash (curl)
Steps:
1. curl -s "http://localhost:4321/api/auth/callback/vk?code=abc&state=wrong"
Expected Result: 401 Unauthorized
Evidence: .sisyphus/evidence/t6-invalid-state.log
```
**Commit**: YES
- Message: `feat(auth): implement oauth pkce for vk and yandex`
- Files: `src/lib/auth/oauth.ts`, `src/pages/api/auth/**/*.ts`
- [x] T7. JWT Sessions + Middleware
**What to do**:
- Установить `jose` для JWT
- Создать `src/lib/auth/jwt.ts`:
- `createToken(userId)` — создание JWT
- `verifyToken(token)` — верификация
- `setAuthCookie(token)` — установка httpOnly cookie
- `clearAuthCookie()` — очистка
- Создать `src/lib/auth/session.ts` — работа с PostgreSQL sessions таблицей
- Создать `src/middleware/auth.ts` — middleware для проверки сессий
- Добавить `Astro.locals.user` типизацию
- TTL сессии: 7 дней
**Must NOT do**:
- Не хранить JWT secret в коде (только env)
- Не использовать `localStorage` для токенов
**Recommended Agent Profile**:
- **Category**: `unspecified-high`
- **Skills**: []
- **Reason**: Security-critical
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 2
- **Blocks**: T13, T14
- **Blocked By**: T1, T2
**References**:
- `jose` library docs: JWT sign, verify, Web Crypto API
- `src/db/schema.ts` — sessions таблица
**Acceptance Criteria**:
- [ ] `createToken()` генерирует валидный JWT
- [ ] `verifyToken()` принимает валидный и отклоняет истёкший
- [ ] Cookie httpOnly, Secure, SameSite=strict
- [ ] Middleware устанавливает `Astro.locals.user` для авторизованных
**QA Scenarios**:
```
Scenario: JWT creation and verification
Tool: Bash (node)
Steps:
1. node -e "const { createToken, verifyToken } = require('./src/lib/auth/jwt.ts'); const t = createToken('user123'); console.log(verifyToken(t).sub)"
Expected Result: "user123"
Evidence: .sisyphus/evidence/t7-jwt.log
Scenario: Expired token rejected
Tool: Bash (node)
Steps:
1. node -e "const { verifyToken } = require('./src/lib/auth/jwt.ts'); verifyToken('expired_token')"
Expected Result: Ошибка "JWT expired"
Evidence: .sisyphus/evidence/t7-jwt-expired.log
```
**Commit**: YES
- Message: `feat(auth): implement jwt sessions + middleware`
- Files: `src/lib/auth/jwt.ts`, `src/lib/auth/session.ts`, `src/middleware/auth.ts`
- [x] T8. Dice Roller Component
**What to do**:
- Создать `src/components/dm/DiceRoller.astro`
- Поддержка нотаций: d4, d6, d8, d10, d12, d20, d100
- Поле ввода: "2d6+3" или кнопки быстрого выбора
- Модификатор (+/-)
- Использовать `crypto.getRandomValues` из `src/lib/client/random.ts`
- Анимация броска (вращение кубика)
- История бросков
- TDD: написать тесты перед реализацией
**Must NOT do**:
- Не делать сервер-side броски (клиент-side только)
- Не добавлять advantage/disadvantage (MVP scope)
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: []
- **Reason**: Простой клиентский компонент
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 2
- **Blocks**: T14
- **Blocked By**: T4, T3
**References**:
- `src/lib/client/random.ts` — `randomInt`, `crypto.getRandomValues`
- `src/components/generators/NumberGenerator.astro` — паттерн генератора
- `src/components/ResultBox.astro` — паттерн отображения результата
**Acceptance Criteria**:
- [ ] "1d20" генерирует число 1-20
- [ ] "2d6" генерирует число 2-12
- [ ] История бросков сохраняется в sessionStorage
- [ ] Анимация отображается при броске
**QA Scenarios**:
```
Scenario: d20 produces valid range
Tool: Playwright
Steps:
1. Открыть /dm/
2. Кликнуть "d20"
3. Проверить результат 1-20
Expected Result: Число в диапазоне [1, 20]
Evidence: .sisyphus/evidence/t8-d20-roll.png
Scenario: Invalid input shows error
Tool: Playwright
Steps:
1. Ввести "abc" в поле dice
2. Кликнуть "Roll"
Expected Result: Сообщение об ошибке "Неверная нотация"
Evidence: .sisyphus/evidence/t8-invalid-dice.png
```
**Commit**: YES
- Message: `feat(dm): add dice roller component`
- Files: `src/components/dm/DiceRoller.astro`, `tests/dice-roller.test.ts`
- [x] T9. Initiative Tracker Component
**What to do**:
- Создать `src/components/dm/InitiativeTracker.astro`
- Форма добавления: Имя + Бросок инициативы (или авто-бросок d20 + модификатор)
- Список combatants, отсортированный по убыванию
- Кнопка "Следующий ход" — подсвечивает активного
- Кнопка "Удалить" для каждого
- Кнопка "Очистить всё"
- Данные в sessionStorage (обнуляются при refresh — MVP)
- TDD: написать тесты перед реализацией
**Must NOT do**:
- Не добавлять HP/статусы/раунды (MVP scope)
- Не сохранять в PostgreSQL (MVP scope)
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: []
- **Reason**: Простой клиентский компонент
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 2
- **Blocks**: T14
- **Blocked By**: T4, T3
**References**:
- `src/lib/client/random.ts` — `randomInt`
- `src/lib/client/animations.ts` — `popElement`
- `src/lib/client/validation.ts` — `createErrorDisplay`
**Acceptance Criteria**:
- [ ] Добавление combatant увеличивает список
- [ ] Сортировка по убыванию броска
- [ ] "Следующий ход" меняет активного
- [ ] Refresh сбрасывает список
**QA Scenarios**:
```
Scenario: Initiative sorts descending
Tool: Playwright
Steps:
1. Добавить "Гоблин" с 15
2. Добавить "Воин" с 18
3. Проверить порядок: Воин, Гоблин
Expected Result: Воин (18) выше Гоблина (15)
Evidence: .sisyphus/evidence/t9-initiative-sort.png
Scenario: Turn cycling works
Tool: Playwright
Steps:
1. Добавить 3 combatants
2. Кликнуть "Следующий ход" 3 раза
3. Проверить, что цикл замкнулся
Expected Result: После 3-го клика активен первый
Evidence: .sisyphus/evidence/t9-initiative-cycle.png
```
**Commit**: YES
- Message: `feat(dm): add initiative tracker component`
- Files: `src/components/dm/InitiativeTracker.astro`, `tests/initiative-tracker.test.ts`
- [x] T10. Open5e API Client + Cache
**What to do**:
- Создать `src/lib/open5e/client.ts`:
- `searchMonsters(query, filters)` — поиск монстров
- `getMonster(key)` — получение по slug
- `searchSpells(query, filters)` — поиск заклинаний
- `getSpell(key)` — получение заклинания
- Создать `src/lib/open5e/cache.ts`:
- Кэширование в localStorage с TTL (24 часа)
- Ключи: `open5e:monsters:query`, `open5e:monster:key`
- Использовать Open5e V2 API с `fields=` parameter
- Обработка ошибок (API недоступен → fallback message)
- TDD: тесты с mock fetch
**Must NOT do**:
- Не кэшировать в IndexedDB (MVP: localStorage достаточно)
- Не делать server-side cache (MVP: client-side only)
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: []
- **Reason**: API клиент + кэш
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 2
- **Blocks**: T11, T14
- **Blocked By**: T1, T3
**References**:
- Open5e V2 API: `https://api.open5e.com/v2/creatures/`, `https://api.open5e.com/v2/spells/`
- Open5e docs: field filtering, pagination
**Acceptance Criteria**:
- [ ] Поиск "dragon" возвращает монстров
- [ ] Кэш сохраняет результаты в localStorage
- [ ] Повторный поиск использует кэш
- [ ] API недоступен → показывается ошибка
**QA Scenarios**:
```
Scenario: Search monsters returns results
Tool: Bash (curl)
Steps:
1. curl -s "https://api.open5e.com/v2/creatures/?name__icontains=dragon&fields=name,key,challenge_rating_decimal"
Expected Result: JSON с массивом существ
Evidence: .sisyphus/evidence/t10-open5e-search.json
Scenario: Cache stores results
Tool: Playwright
Steps:
1. Открыть /dm/
2. Поискать "goblin"
3. Проверить localStorage
Expected Result: localStorage содержит ключ open5e:*
Evidence: .sisyphus/evidence/t10-open5e-cache.log
```
**Commit**: YES
- Message: `feat(dm): add open5e api client + localstorage cache`
- Files: `src/lib/open5e/client.ts`, `src/lib/open5e/cache.ts`, `tests/open5e-client.test.ts`
- [x] T11. Open5e Search UI
**What to do**:
- Создать `src/components/dm/Open5eReference.astro`
- Табы: Монстры / Заклинания
- Поисковая строка с debounce (300ms)
- Фильтры: CR (для монстров), Уровень (для заклинаний), Тип
- Список результатов с пагинацией
- Карточка деталей (модалка или раскрывающаяся карточка)
- TDD: тесты на поиск и фильтрацию
**Must NOT do**:
- Не добавлять закладки (Итерация 2)
- Не добавлять "добавить в энкаунтер" (Итерация 2)
**Recommended Agent Profile**:
- **Category**: `visual-engineering`
- **Skills**: [`/frontend-ui-ux`]
- **Reason**: UI компонент с поиском
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 2
- **Blocks**: T14
- **Blocked By**: T4, T10, T3
**References**:
- `src/components/dm/Open5eReference.astro` (новый)
- `src/lib/open5e/client.ts` — API клиент
- `src/components/GeneratorCard.astro` — паттерн карточки
**Acceptance Criteria**:
- [ ] Поиск "goblin" отображает гоблинов
- [ ] Фильтр по CR работает
- [ ] Карточка деталей открывается по клику
- [ ] Пагинация переключает страницы
**QA Scenarios**:
```
Scenario: Search goblins and view details
Tool: Playwright
Steps:
1. Открыть /dm/
2. Перейти в таб "Монстры"
3. Ввести "goblin" в поиск
4. Кликнуть на первый результат
Expected Result: Открывается карточка с HP, AC, действиями
Evidence: .sisyphus/evidence/t11-open5e-details.png
Scenario: Filter by CR
Tool: Playwright
Steps:
1. Выбрать фильтр CR: 1/4
2. Проверить, что все результаты имеют CR 1/4
Expected Result: Только существа с CR 1/4
Evidence: .sisyphus/evidence/t11-open5e-filter.png
```
**Commit**: YES
- Message: `feat(dm): add open5e search ui`
- Files: `src/components/dm/Open5eReference.astro`, `tests/open5e-ui.test.ts`
- [x] T12. Notes Component
**What to do**:
- Создать `src/components/dm/NotesPanel.astro`
- Текстовая область для заметок
- Автосохранение в localStorage (debounce 500ms)
- Индикатор "Сохранено"
- Кнопка "Очистить"
- Несколько заметок? (MVP: одна большая заметка)
- TDD: тесты на localStorage persistence
**Must NOT do**:
- Не сохранять в PostgreSQL (MVP scope)
- Не добавлять rich text/Markdown (MVP: plain text)
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: []
- **Reason**: Простой клиентский компонент
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 2
- **Blocks**: T14
- **Blocked By**: T4, T3
**References**:
- `src/lib/client/clipboard.ts` — паттерны клиентских утилит
- `src/components/ResultBox.astro` — паттерн панели
**Acceptance Criteria**:
- [ ] Текст сохраняется в localStorage
- [ ] После refresh текст восстанавливается
- [ ] Индикатор "Сохранено" появляется через 500ms
**QA Scenarios**:
```
Scenario: Notes persist across refresh
Tool: Playwright
Steps:
1. Открыть /dm/
2. Ввести "Тестовая заметка" в поле
3. Подождать 1 секунду
4. Refresh страницы
Expected Result: Текст "Тестовая заметка" остался
Evidence: .sisyphus/evidence/t12-notes-persist.png
Scenario: Clear button empties notes
Tool: Playwright
Steps:
1. Ввести текст
2. Кликнуть "Очистить"
3. Проверить localStorage
Expected Result: localStorage пустой
Evidence: .sisyphus/evidence/t12-notes-clear.png
```
**Commit**: YES
- Message: `feat(dm): add notes panel with localstorage`
- Files: `src/components/dm/NotesPanel.astro`, `tests/notes.test.ts`
- [x] T13. Auth UI (Login / Logout / Profile)
**What to do**:
- Создать `src/components/dm/AuthPanel.astro`
- Кнопки "Войти через VK" и "Войти через Yandex"
- После входа: аватар + имя + кнопка "Выйти"
- Получение данных пользователя из Astro.locals.user
- Показывать гостевое состояние для неавторизованных
- TDD: тесты на отображение состояний
**Must NOT do**:
- Не показывать email/чувствительные данные
- Не добавлять регистрацию по email (только OAuth)
**Recommended Agent Profile**:
- **Category**: `visual-engineering`
- **Skills**: [`/frontend-ui-ux`]
- **Reason**: UI компонент аутентификации
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 2
- **Blocks**: T14
- **Blocked By**: T6, T7, T4
**References**:
- `src/components/LanguageSwitcher.astro` — паттерн верхней панели
- `src/layouts/BaseLayout.astro` — интеграция в layout
**Acceptance Criteria**:
- [ ] Неавторизованный видит кнопки входа
- [ ] Авторизованный видет аватар и имя
- [ ] Клик "Выйти" очищает сессию
**QA Scenarios**:
```
Scenario: Guest sees login buttons
Tool: Playwright
Steps:
1. Открыть /dm/ в инкогнито
2. Проверить наличие "Войти через VK"
Expected Result: Кнопки входа видны
Evidence: .sisyphus/evidence/t13-guest-view.png
Scenario: Logout clears session
Tool: Playwright
Steps:
1. Авторизоваться (mock сессия)
2. Кликнуть "Выйти"
3. Проверить cookies
Expected Result: Cookie auth очищен, показывается гостевое состояние
Evidence: .sisyphus/evidence/t13-logout.png
```
**Commit**: YES
- Message: `feat(dm): add auth ui panel`
- Files: `src/components/dm/AuthPanel.astro`, `tests/auth-ui.test.ts`
- [x] T14. DM Dashboard Layout + Page
**What to do**:
- Создать `src/layouts/DmLayout.astro`:
- Наследует BaseLayout (SEO, analytics)
- Оранжевая тема (подключает dm-theme.css)
- Хедер с логотипом DM Dashboard + AuthPanel
- Основной слот для контента
- Футер с ссылками
- Создать `src/pages/dm/index.astro`:
- Использует DmLayout
- Отображает все инструменты: DiceRoller, InitiativeTracker, Open5eReference, NotesPanel
- Табы или секции для переключения между инструментами
- RU i18n через `src/i18n/dm-translations.ts`
- Добавить `src/i18n/dm-translations.ts` со всеми строками DM Dashboard
**Must NOT do**:
- Не менять существующие layouts (BaseLayout, GeneratorLayout)
- Не добавлять sidebar или сложную навигацию (MVP: простая страница)
**Recommended Agent Profile**:
- **Category**: `deep`
- **Skills**: [`/frontend-ui-ux`]
- **Reason**: Интеграция всех компонентов в единый layout
**Parallelization**:
- **Can Run In Parallel**: NO (зависит от многих задач)
- **Parallel Group**: Wave 3
- **Blocks**: T15, T16, T17
- **Blocked By**: T4, T6, T7, T8, T9, T10, T11, T12, T13
**References**:
- `src/layouts/GeneratorLayout.astro` — паттерн обёртки
- `src/layouts/BaseLayout.astro` — базовый layout
- `src/pages/generators/numbers.astro` — паттерн страницы
**Acceptance Criteria**:
- [ ] `/dm/` открывается без ошибок
- [ ] Все 4 инструмента отображаются
- [ ] AuthPanel показывает правильное состояние
- [ ] Оранжевая тема применена
- [ ] RU переводы работают
**QA Scenarios**:
```
Scenario: DM Dashboard loads all tools
Tool: Playwright
Steps:
1. Открыть http://localhost:4321/dm/
2. Сделать скриншот
3. Проверить наличие: Кубики, Инициатива, Справочник, Заметки
Expected Result: Все 4 секции видны, оформление оранжевое
Evidence: .sisyphus/evidence/t14-dashboard-full.png
Scenario: Auth state reflected
Tool: Playwright
Steps:
1. Открыть /dm/ в инкогнито
2. Проверить "Войти через VK"
3. Авторизоваться (mock)
4. Проверить имя пользователя
Expected Result: После входа показывается имя и аватар
Evidence: .sisyphus/evidence/t14-auth-state.png
```
**Commit**: YES
- Message: `feat(dm): integrate dashboard layout + page`
- Files: `src/layouts/DmLayout.astro`, `src/pages/dm/index.astro`, `src/i18n/dm-translations.ts`
- [x] T15. Navigation + Routing
**What to do**:
- Добавить ссылку на DM Dashboard в главное меню (LanguageSwitcher или отдельная кнопка)
- Создать `src/pages/dm/dice.astro` — отдельная страница кубиков (опционально)
- Создать `src/pages/dm/initiative.astro` — отдельная страница инициативы (опционально)
- Создать `src/pages/dm/reference.astro` — отдельная страница справочника (опционально)
- Создать `src/pages/dm/notes.astro` — отдельная страница заметок (опционально)
- Или: сделать навигацию по якорям на главной странице `#dice`, `#initiative`, `#reference`, `#notes`
- Обновить `src/pages/index.astro` — добавить карточку DM Dashboard среди генераторов
**Must NOT do**:
- Не создавать дублирующих маршрутов
- Не ломать существующую навигацию
**Recommended Agent Profile**:
- **Category**: `quick`
- **Skills**: []
- **Reason**: Навигация и роутинг
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 3
- **Blocks**: None
- **Blocked By**: T14
**References**:
- `src/pages/index.astro` — главная страница со списком генераторов
- `src/components/GeneratorCard.astro` — карточка генератора
**Acceptance Criteria**:
- [ ] С главной страницы есть ссылка на /dm/
- [ ] С /dm/ можно вернуться на главную
- [ ] Навигация между инструментами работает (якоря или табы)
**QA Scenarios**:
```
Scenario: Navigation from homepage
Tool: Playwright
Steps:
1. Открыть /
2. Кликнуть "DM Dashboard"
3. Проверить URL
Expected Result: URL = /dm/
Evidence: .sisyphus/evidence/t15-nav-home-to-dm.png
```
**Commit**: YES
- Message: `feat(dm): add navigation and routing`
- Files: `src/pages/dm/*.astro`, `src/pages/index.astro`
- [x] T16. Build Verification + CI Update
**What to do**:
- Обновить `.github/workflows/deploy.yml`:
- Добавить шаг запуска PostgreSQL (docker-compose)
- Добавить миграции перед деплоем
- Обновить rsync для Node.js приложения (или перейти на docker deploy)
- Проверить `npm run build` — нет ошибок
- Проверить `npm run test` — все тесты проходят
- Проверить `npm run lint` — нет ошибок
- Добавить health-check endpoint `/api/health`
- Обновить `docker-compose.yml` для production
**Must NOT do**:
- Не удалять существующий деплой основного сайта
- Не коммитить secrets в CI
**Recommended Agent Profile**:
- **Category**: `unspecified-high`
- **Skills**: []
- **Reason**: CI/CD, деплой
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 3
- **Blocks**: None
- **Blocked By**: T1, T14
**References**:
- `.github/workflows/deploy.yml` — текущий workflow
- `docker-compose.yml` — Docker конфигурация
- `Dockerfile` — обновлённый образ
**Acceptance Criteria**:
- [ ] `npm run build` — SUCCESS
- [ ] `npm run test` — все тесты PASS
- [ ] `npm run lint` — нет ошибок
- [ ] Docker compose запускает app + postgres
- [ ] CI workflow проходит
**QA Scenarios**:
```
Scenario: Full build succeeds
Tool: Bash
Steps:
1. npm run build
2. npm run test
3. npm run lint
Expected Result: Все команды завершаются с кодом 0
Evidence: .sisyphus/evidence/t16-build.log
Scenario: Docker compose health check
Tool: Bash
Steps:
1. docker compose up -d
2. curl -s http://localhost:4321/api/health
Expected Result: {"status":"ok"}
Evidence: .sisyphus/evidence/t16-health.json
```
**Commit**: YES
- Message: `ci(dm): update build, tests, and deployment`
- Files: `.github/workflows/deploy.yml`, `docker-compose.yml`, `src/pages/api/health.ts`
- [x] T17. Final Styling Polish
**What to do**:
- Проверить responsive design (mobile, tablet, desktop)
- Проверить accessibility (focus-visible, aria-labels)
- Оптимизировать шрифты и spacing
- Проверить консистентность оранжевой темы
- Добавить hover/active состояния для интерактивных элементов
- Проверить тёмную тему (контрастность)
- TDD: визуальные регрессионные тесты (скриншоты)
**Must NOT do**:
- Не менять функционал
- Не добавлять новые фичи
**Recommended Agent Profile**:
- **Category**: `visual-engineering`
- **Skills**: [`/frontend-ui-ux`]
- **Reason**: UI полировка
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 3
- **Blocks**: None
- **Blocked By**: T14, T15
**References**:
- `src/styles/dm-theme.css` — текущая тема
- Tailwind CSS docs: responsive design, accessibility
**Acceptance Criteria**:
- [ ] Mobile: все элементы помещаются на экране 375px
- [ ] Desktop: адекватные отступы и размеры
- [ ] Focus-visible работает на всех интерактивных элементах
- [ ] Контрастность текста ≥ 4.5:1
**QA Scenarios**:
```
Scenario: Responsive design
Tool: Playwright
Steps:
1. Открыть /dm/ в viewport 375x667
2. Сделать скриншот
3. Открыть /dm/ в viewport 1920x1080
4. Сделать скриншот
Expected Result: Оба скриншота показывают удобочитаемый интерфейс
Evidence: .sisyphus/evidence/t17-responsive-mobile.png, t17-responsive-desktop.png
```
**Commit**: YES
- Message: `style(dm): final polish and responsive design`
- Files: `src/styles/dm-theme.css`, `src/components/dm/*.astro`
---
## Final Verification Wave
> 4 review agents run in PARALLEL. ALL must APPROVE. Present consolidated results to user and get explicit "okay" before completing.
- [x] F1. **Plan Compliance Audit** — `oracle`
Read the plan end-to-end. For each "Must Have": verify implementation exists. For each "Must NOT Have": search codebase for forbidden patterns. Check evidence files exist.
Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT`
- [x] F2. **Code Quality Review** — `unspecified-high`
Run `tsc --noEmit` + `npm run lint` + `npm run test`. Review changed files for AI slop patterns.
Output: `Build [PASS/FAIL] | Lint [PASS/FAIL] | Tests [N pass/N fail] | VERDICT`
- [x] F3. **Real Manual QA** — `unspecified-high` (+ `playwright` skill)
Execute EVERY QA scenario from EVERY task. Test cross-task integration. Capture evidence.
Output: `Scenarios [N/N pass] | Integration [N/N] | VERDICT`
- [x] F4. **Scope Fidelity Check** — `deep`
For each task: read "What to do", read actual diff. Verify 1:1. Check "Must NOT do" compliance.
Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | VERDICT`
---
## Plan Reviews
### Metis Review
- **Status**: COMPLETED
- **Findings**: Core Objective разделён на MVP и Product Vision; Scope IN/OUT определены; противоречия задокументированы.
### Oracle Verification Phase 1
- **Status**: PASS
- **Verdict**: CHECK 5/5 PASS | GO
### Oracle Verification Phase 2
- **Status**: PASS
- **Verdict**: CHECK 6/6 PASS | GO
### Momus Review
- **Status**: OKAY
- **Verdict**: План хорошо структурирован, все ссылки валидны, каждая задача имеет конкретную точку входа, чёткие критерии приёмки и исполняемые QA-сценарии.
### Oracle Verification Phase 3
- **Status**: PASS (after fixes)
- **Verdict**: CHECK 5/5 PASS | GO
## Commit Strategy
- **T1-T5**: `chore(dm): setup hybrid astro + docker + db + tests + theme`
- **T6-T7**: `feat(auth): implement oauth + jwt sessions`
- **T8-T13**: `feat(dm): add dice, initiative, open5e, notes, auth ui`
- **T14-T17**: `feat(dm): integrate dashboard layout + navigation + polish`
- **F1-F4**: `chore(dm): final verification and fixes`
---
## Success Criteria
### Verification Commands
```bash
# Build
npm run build
# Tests
npm run test
# Lint
npm run lint
# Docker
docker compose up -d
# OAuth callback routes exist
curl -s http://localhost:4321/api/auth/callback/vk -w "%{http_code}"
curl -s http://localhost:4321/api/auth/callback/yandex -w "%{http_code}"
# DM Dashboard page loads
curl -s http://localhost:4321/dm/ | grep -q "DM Dashboard"
# Database migrations
docker compose exec postgres psql -U dmuser -d dmdashboard -c "\dt"
```
### Final Checklist
- [x] All "Must Have" present
- [x] All "Must NOT Have" absent
- [x] All tests pass
- [x] Build succeeds
- [x] Docker compose запускается
- [x] OAuth callbacks отвечают
- [x] DM Dashboard страница доступна
- [x] Таблицы в PostgreSQL созданы