Add TECHNICAL.md: infrastructure, design tokens, media specs, DB schema, done stages
This commit is contained in:
+258
@@ -0,0 +1,258 @@
|
||||
# TECHNICAL — LMS Second Brain
|
||||
|
||||
Живая документация проекта. Обновляется по мере разработки.
|
||||
Роадмап и планирование — в `ROADMAP.md`. Здесь — факты о том, как всё устроено.
|
||||
|
||||
---
|
||||
|
||||
## Инфраструктура
|
||||
|
||||
| Компонент | Значение |
|
||||
|---|---|
|
||||
| **Сервер** | Hetzner VPS — 8 vCPU / 16 GB RAM / 320 GB NVMe |
|
||||
| **IP** | 178.104.27.196 |
|
||||
| **Домен LMS** | https://school.second-brain.ru |
|
||||
| **Reverse proxy** | Caddy (auto HTTPS через Let's Encrypt) |
|
||||
| **Порт приложения** | 3010 (внутри контейнера — 3000) |
|
||||
| **БД** | PostgreSQL 16 (контейнер `lms-sb-db-1`) |
|
||||
| **Object Storage** | Hetzner Object Storage, регион Nuremberg |
|
||||
| **Бакет** | `second-brain-lms` (публичный, read-only) |
|
||||
| **Endpoint S3** | https://nbg1.your-objectstorage.com |
|
||||
| **Git-репозиторий** | https://git.second-brain.ru/admins/lms-sb |
|
||||
|
||||
### Деплой
|
||||
|
||||
```bash
|
||||
# На сервере: /root/digital-household/lms-sb/
|
||||
git pull ...
|
||||
docker compose -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
При старте контейнер автоматически запускает `prisma migrate deploy`, затем `node server.js`.
|
||||
|
||||
### .env на сервере
|
||||
|
||||
Файл `/root/digital-household/lms-sb/.env`:
|
||||
|
||||
```
|
||||
DB_PASSWORD=lms_cd5041e961a3050db359aa15
|
||||
BETTER_AUTH_SECRET=<secret>
|
||||
RESEND_API_KEY=<пусто — заполнить при настройке email>
|
||||
EMAIL_FROM=noreply@school.second-brain.ru
|
||||
S3_ENDPOINT=https://nbg1.your-objectstorage.com
|
||||
S3_BUCKET=second-brain-lms
|
||||
S3_ACCESS_KEY=<ключ>
|
||||
S3_SECRET_KEY=<секрет>
|
||||
S3_REGION=eu-central
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Стек
|
||||
|
||||
| Слой | Технология | Версия |
|
||||
|---|---|---|
|
||||
| Фреймворк | Next.js (App Router) | 16.2.2 |
|
||||
| Язык | TypeScript | 5.x |
|
||||
| UI | React | 19 |
|
||||
| Стили | Tailwind CSS (CSS-based config) | 4.x |
|
||||
| UI-компоненты | shadcn/ui (базируется на Base UI, **не Radix**) | latest |
|
||||
| ORM | Prisma | 7.x |
|
||||
| Auth | Better Auth | 1.6.0 |
|
||||
| WYSIWYG | TipTap | 2.x |
|
||||
| Drag-and-drop | @dnd-kit | latest |
|
||||
| Шрифт | Fira Mono (400/500/700, Latin + Cyrillic) | Google Fonts |
|
||||
| Email | Resend | latest |
|
||||
| S3 | @aws-sdk/client-s3 | 3.x |
|
||||
| БД | PostgreSQL | 16 |
|
||||
|
||||
### Важные нюансы стека
|
||||
|
||||
- **shadcn/ui v4** использует `@base-ui/react`, а не Radix. Нет `asChild`. Триггеры — обычные элементы.
|
||||
- **Prisma 7** не генерирует `index.ts`. Импорт: `from "@/generated/prisma/client"`, не `from "@/generated/prisma"`.
|
||||
- **Prisma 7** требует адаптер: `new PrismaPg({ connectionString })` — иначе `PrismaClient()` бросает ошибку.
|
||||
- **Better Auth** использует `scrypt` по умолчанию. В этом проекте **переключён на bcrypt** (в `auth.ts` настроены `password.hash` / `password.verify`).
|
||||
- **`NEXT_PUBLIC_*`** переменные запекаются при сборке. `auth-client.ts` не использует `baseURL` — клиент сам берёт `window.location.origin`.
|
||||
- **Next.js 16** использует `proxy.ts` вместо `middleware.ts` (и экспортируемая функция называется `proxy`, не `middleware`).
|
||||
- **Tailwind v4**: конфиг только в CSS через `@import "tailwindcss"` и `@theme`. Нет `tailwind.config.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Дизайн-система
|
||||
|
||||
Стиль: **Second Brain Aubade** — типографский, монохромный, с газетным характером.
|
||||
|
||||
| Токен | Значение |
|
||||
|---|---|
|
||||
| Шрифт | Fira Mono (весь UI) |
|
||||
| Фон страницы | `#F5F5F0` (тёплый off-white) |
|
||||
| Текст основной | `#323232` (тёмный уголь) |
|
||||
| Текст вторичный | `#666666` |
|
||||
| Поверхность / surface | `#E8E8E0` |
|
||||
| Акцент / highlight | `#E8F0D8` (зелёный) |
|
||||
| Divider / border | `#AAAAAA` |
|
||||
| Hover | `#D8D8D0` |
|
||||
| Фон сайдбара (тёмный) | `#2A2A28` |
|
||||
| Активный пункт сайдбара | `#E8F0D8` (зелёный) |
|
||||
|
||||
**Aubade-эффект** — фирменный стиль карточек и кнопок:
|
||||
- Border: `2px solid #AAAAAA`
|
||||
- Box-shadow: `4px 4px 0 0 #AAAAAA` (смещение без размытия)
|
||||
- Hover: `transform: translate(-2px, -2px)` + shadow `6px 6px`
|
||||
- Active (кнопка): `transform: translate(2px, 2px)` + shadow убирается
|
||||
|
||||
CSS-классы: `.card-aubade`, `.btn-aubade`, `.btn-aubade-accent`, `.tag-aubade`
|
||||
|
||||
---
|
||||
|
||||
## Требования к медиафайлам
|
||||
|
||||
### Обложка курса (`Course.coverImage`)
|
||||
|
||||
| Параметр | Требование |
|
||||
|---|---|
|
||||
| **Соотношение сторон** | **16 : 9** (горизонтальный прямоугольник) |
|
||||
| **Рекомендуемое разрешение** | 1280 × 720 px (HD) или 1920 × 1080 px (Full HD) |
|
||||
| **Минимальное разрешение** | 800 × 450 px |
|
||||
| **Максимальный размер файла** | 5 MB |
|
||||
| **Форматы** | JPG, PNG, WebP |
|
||||
| **Цветовое пространство** | sRGB |
|
||||
| **Где хранится** | Hetzner Object Storage, бакет `second-brain-lms`, путь `uploads/<uuid>.ext` |
|
||||
| **Доступ** | Публичный URL (прямая ссылка на файл) |
|
||||
|
||||
> Пример URL: `https://nbg1.your-objectstorage.com/second-brain-lms/uploads/abc123.jpg`
|
||||
|
||||
### Изображения в уроках (TipTap)
|
||||
|
||||
| Параметр | Требование |
|
||||
|---|---|
|
||||
| **Соотношение сторон** | Любое — TipTap встраивает как `<img>` с `max-width: 100%` |
|
||||
| **Рекомендуемая ширина** | 1200 px (контент-зона урока) |
|
||||
| **Максимальный размер файла** | 10 MB |
|
||||
| **Форматы** | JPG, PNG, GIF, WebP |
|
||||
| **Где хранится** | Hetzner Object Storage, путь `uploads/<uuid>.ext` |
|
||||
|
||||
### PDF и файлы к уроку (Этап 2+)
|
||||
|
||||
| Параметр | Требование |
|
||||
|---|---|
|
||||
| **Форматы** | PDF, ZIP, DOCX, XLSX, PPTX |
|
||||
| **Максимальный размер** | 100 MB |
|
||||
| **Где хранится** | Hetzner Object Storage, путь `lessons/<lessonId>/files/<uuid>.ext` |
|
||||
|
||||
### Аватары пользователей (если добавим)
|
||||
|
||||
| Параметр | Требование |
|
||||
|---|---|
|
||||
| **Соотношение сторон** | 1 : 1 (квадрат) |
|
||||
| **Рекомендуемый размер** | 256 × 256 px |
|
||||
| **Максимальный размер файла** | 2 MB |
|
||||
| **Форматы** | JPG, PNG, WebP |
|
||||
|
||||
---
|
||||
|
||||
## Роли и доступ
|
||||
|
||||
| Роль | Маршруты | Описание |
|
||||
|---|---|---|
|
||||
| `admin` | `/admin/*`, `/curator/*`, `/dashboard` | Полный доступ |
|
||||
| `curator` | `/curator/*`, `/dashboard` | Проверка ДЗ, комментарии |
|
||||
| `student` | `/dashboard`, `/courses/*` | Просмотр курсов, прогресс |
|
||||
|
||||
Защита маршрутов — в `src/proxy.ts` + проверка сессии в каждом layout/page.
|
||||
|
||||
---
|
||||
|
||||
## API-маршруты
|
||||
|
||||
| Метод | Путь | Описание | Кто может |
|
||||
|---|---|---|---|
|
||||
| `POST` | `/api/auth/[...all]` | Better Auth handler | Все |
|
||||
| `POST` | `/api/admin/upload` | Загрузка файла в S3, возвращает `{ url, key }` | admin |
|
||||
|
||||
---
|
||||
|
||||
## Структура БД (ключевые таблицы)
|
||||
|
||||
```
|
||||
User — id, email, name, role, emailVerified
|
||||
Session — Better Auth sessions
|
||||
Account — Better Auth credentials (bcrypt password)
|
||||
Verification — Better Auth email verification tokens
|
||||
|
||||
Category — id, title, slug, order
|
||||
Course — id, slug, title, description, coverImage, published, order, categoryId
|
||||
Module — id, courseId, title, order
|
||||
Lesson — id, moduleId, title, content (JSON), kinescopeId, published, order
|
||||
LessonFile — id, lessonId, name, url, size
|
||||
|
||||
CourseEnrollment — userId + courseId (PK), enrolledAt, expiresAt
|
||||
AccessLog — id, courseId, userId, action, method, grantedById, note, createdAt
|
||||
LessonProgress — userId + lessonId (PK), completedAt
|
||||
|
||||
Quiz — id, lessonId, showAnswers
|
||||
QuizQuestion — id, quizId, text, type (SINGLE/MULTIPLE/TEXT), order
|
||||
QuizOption — id, questionId, text, isCorrect, order
|
||||
QuizAttempt — id, userId, quizId, score, answers (JSON), completedAt
|
||||
|
||||
Homework — id, lessonId, description
|
||||
HomeworkSubmission — id, homeworkId, userId, text, files (JSON), submittedAt
|
||||
HomeworkFeedback — id, submissionId, curatorId, text, createdAt
|
||||
|
||||
LessonComment — id, lessonId, userId, text, deleted, createdAt
|
||||
```
|
||||
|
||||
Миграции: `prisma/migrations/` — **никогда не редактировать вручную**.
|
||||
|
||||
---
|
||||
|
||||
## Тестовые аккаунты (seed)
|
||||
|
||||
| Email | Пароль | Роль |
|
||||
|---|---|---|
|
||||
| admin@second-brain.ru | Password123! | admin |
|
||||
| curator@second-brain.ru | Password123! | curator |
|
||||
| student@second-brain.ru | Password123! | student |
|
||||
|
||||
---
|
||||
|
||||
## Что сделано (по этапам)
|
||||
|
||||
### Этап 0 — Каркас + Auth ✅
|
||||
- Next.js 16.2.2 + TypeScript + Tailwind v4
|
||||
- PostgreSQL 16 + Prisma 7 + полная LMS-схема
|
||||
- Better Auth: email/password, роли, сессии
|
||||
- proxy.ts: защита маршрутов
|
||||
- Дашборды для 3 ролей (admin / curator / student)
|
||||
- Dockerfile multi-stage + docker-compose.prod.yml
|
||||
- Caddy: school.second-brain.ru → порт 3010
|
||||
|
||||
### Этап 1 — CRUD курсов в админке ✅
|
||||
- Список курсов: `/admin/courses`
|
||||
- Создание курса (диалог), редактирование, удаление
|
||||
- Обложка курса: загрузка в S3, требования — см. раздел «Медиафайлы»
|
||||
- Модули: drag-and-drop сортировка, CRUD
|
||||
- Уроки: drag-and-drop сортировка, CRUD
|
||||
- Редактор урока: TipTap (Bold, Italic, H2/H3, списки, цитата, код, ссылки, изображения)
|
||||
- Загрузка изображений в урок → S3
|
||||
- Поле Kinescope ID (текстовое)
|
||||
- Публикация / скрытие курса и урока
|
||||
- Управление доступом к курсу (выдать / отозвать)
|
||||
- Страница пользователей: `/admin/users`
|
||||
- Дизайн Second Brain Aubade (Fira Mono, #F5F5F0, карточки с тенью)
|
||||
|
||||
### Этап 1.5 — Расширенное управление доступом ✅
|
||||
- Категории курсов: `/admin/categories`, CRUD, привязка к курсу
|
||||
- Срок доступа: поле `expiresAt` при энролле, просроченный подсвечивается красным
|
||||
- Страница ученика `/admin/users/[userId]`: мультиэнролл (несколько курсов + срок)
|
||||
- История доступа: таблица `AccessLog`, отображается на странице курса и ученика
|
||||
- Hetzner Object Storage подключён: бакет `second-brain-lms`, Nuremberg
|
||||
|
||||
---
|
||||
|
||||
## Известные ограничения / технический долг
|
||||
|
||||
- `requireEmailVerification: true` в Better Auth — seed-пользователи вставлены напрямую через SQL с `emailVerified = true`
|
||||
- Загрузка файлов через `/api/admin/upload` — нет ограничения по размеру на уровне Next.js (только S3). При необходимости добавить middleware с проверкой `Content-Length`
|
||||
- Drag-and-drop обновляет порядок через Server Actions — при быстрых последовательных перетаскиваниях возможны race conditions (некритично для MVP)
|
||||
- `expiresAt` проверяется только в UI (красная подсветка). Блокировка доступа по сроку на уровне middleware — в рамках Этапа 2
|
||||
Reference in New Issue
Block a user