Files
lms-sb/TECHNICAL.md
T

259 lines
12 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.
# 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