# 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 | | **Email-сервис** | Resend, домен `mailsend.second-brain.ru` (verified) | | **From-адрес** | noreply@mailsend.second-brain.ru | ### Деплой ```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= RESEND_API_KEY=re_VX7TCnjs_N2geRqvveHjVfbsSyr4VbmzL EMAIL_FROM=noreply@mailsend.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/.ext` | | **Доступ** | Публичный URL (прямая ссылка на файл) | > Пример URL: `https://nbg1.your-objectstorage.com/second-brain-lms/uploads/abc123.jpg` ### Изображения в уроках (TipTap) | Параметр | Требование | |---|---| | **Соотношение сторон** | Любое — TipTap встраивает как `` с `max-width: 100%` | | **Рекомендуемая ширина** | 1200 px (контент-зона урока) | | **Максимальный размер файла** | 10 MB | | **Форматы** | JPG, PNG, GIF, WebP | | **Где хранится** | Hetzner Object Storage, путь `uploads/.ext` | ### PDF и файлы к уроку (Этап 2+) | Параметр | Требование | |---|---| | **Форматы** | PDF, ZIP, DOCX, XLSX, PPTX | | **Максимальный размер** | 100 MB | | **Где хранится** | Hetzner Object Storage, путь `lessons//files/.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 ### Этап 3 — Прогресс, ДЗ, Email ✅ - Прогресс студента: кнопка «Отметить как пройденный», галочки и прогресс-бар в сайдбаре, прогресс на дашборде - Домашние задания: редактор ДЗ в уроке (admin), сдача текстом + файлами (student), проверка и фидбек (curator/admin) - Куратор-панель: `/curator/dashboard` со статистикой, `/curator/homework` список, страница проверки - Админ имеет доступ к куратор-маршрутам через пункт «ДЗ на проверку» в сайдбаре - Email уведомления через Resend: доступ к курсу, новая работа, фидбек получен, приветствие --- ## Известные ограничения / технический долг - `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