From 03e39723886ab5aad3812889e87835a72f2674e3 Mon Sep 17 00:00:00 2001 From: dmitriylaukhin Date: Tue, 7 Apr 2026 12:08:44 +0500 Subject: [PATCH] Add TECHNICAL.md: infrastructure, design tokens, media specs, DB schema, done stages --- TECHNICAL.md | 258 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 TECHNICAL.md diff --git a/TECHNICAL.md b/TECHNICAL.md new file mode 100644 index 0000000..1a8e39c --- /dev/null +++ b/TECHNICAL.md @@ -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= +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/.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 + +--- + +## Известные ограничения / технический долг + +- `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