Files
lms-sb/AGENTS.md
T
adminsandClaude Opus 5 5ad0bd98f1 Document clean-pdf state, correct SSRF limits and prod gate
Audit of the shipped Chistyy PDF feature against the actual code found
three documentation defects and one misplaced gate:

- The SSRF write-up understated the hole. assertPublicUrl resolves DNS
  exactly once, for the initial URL; the in-browser filter never resolves
  hostnames at all. Any new hostname after the first navigation (redirect,
  subresource, fetch, ws://) goes unchecked - a DNS rebind is not even
  required. Corrected in TECHNICAL.md and the design spec.
- The prod gate was tied to TOOLBOX_VISIBLE, but /api/pdf sits in
  PUBLIC_ROUTES and authenticates itself, so the feature goes live the
  moment browserless and BROWSER_WS_URL appear on prod - before the flag.
  Gate is now tied to the renderer.
- TECHNICAL.md claimed the browserless port is published on neither
  staging nor prod. It is published on dev/staging (127.0.0.1:3333) and
  the SSH tunnel depends on it.
- AGENTS.md described a src/proxy.ts that does not exist; route protection
  lives in src/middleware.ts.

Also adds a state snapshot (docs/plans) and a "grabli uklada" section to
CLAUDE.md covering the non-obvious conventions already enforced in code:
the two ToolUsage ids, the vitest include pattern, page.pdf() without a
timeout option, context.route not seeing WebSockets, and NEXT_PUBLIC_*
being inlined at build time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sy7vY7WQ1A3q1MkgsDd8VB
2026-09-12 12:55:46 +05:00

307 lines
14 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.
# AGENTS.md — LMS Second Brain
Собственная LMS-платформа для образовательных курсов по PKM и Obsidian.
Заменяет emdesell.ru. Масштаб: ~1000 аккаунтов, ~200 активных, до 10 курсов.
Production: **https://school.second-brain.ru**
> Подробная техническая документация — в `TECHNICAL.md`.
> Роадмап и текущий статус — в `ROADMAP.md`.
> Полные правила для Claude Code — в `CLAUDE.md`.
---
## Стек
| Слой | Технология | Версия |
|------|-----------|--------|
| Фреймворк | Next.js (App Router) | **16.2.2** |
| Язык | TypeScript (strict) | 5.x |
| UI | React | 19 |
| Стили | Tailwind CSS (CSS-based, **без** tailwind.config.ts) | 4.x |
| Компоненты | shadcn/ui (Base UI, **не Radix**) | v4 |
| ORM | Prisma | 7.x |
| Auth | Better Auth (**не NextAuth**) | 1.6.0 |
| Редактор | TipTap WYSIWYG | 2.x |
| Drag-and-drop | @dnd-kit | latest |
| БД | PostgreSQL | 16 |
| Email | Resend | latest |
| Хранилище | Hetzner Object Storage (S3-совместимый) | — |
| Видео | Kinescope (iframe embed) | — |
| Валидация | Zod | 3.x |
---
## Критические отличия от стандартных версий
Эти технологии отличаются от того, что содержится в обучающих данных большинства моделей. **Читай документацию перед написанием кода.**
### Next.js 16.2.2
- Защита маршрутов — `src/proxy.ts` **НЕ существует**: весь код в `src/middleware.ts`, экспорт называется `middleware` (проверено 20260912). Next.js 16 поддерживает `proxy.ts`, но в этом проекте он не заведён — не создавать второй файл, править `middleware.ts`
- Перед написанием кода смотри `node_modules/next/dist/docs/`
### Tailwind CSS v4
- **Нет файла `tailwind.config.ts`** — вся кастомизация через CSS
- Конфиг: `@import "tailwindcss"` и `@theme` в `globals.css`
### shadcn/ui v4
- Базируется на `@base-ui/react`, **не Radix**
- Нет пропа `asChild` — триггеры обычные элементы
- Установка: `npx shadcn@latest add <component>`
### Prisma 7.x
- Импорт: `from "@/generated/prisma/client"` (не `from "@/generated/prisma"`)
- Требует адаптер: `new PrismaPg({ connectionString })`
- Не генерирует `index.ts`
### Better Auth 1.6.0
- **Не путать с NextAuth** — другая библиотека, другое API
- В этом проекте используется **bcrypt** (не scrypt по умолчанию)
- Настройки `password.hash` / `password.verify` в `src/lib/auth.ts`
- `auth-client.ts` не использует `baseURL` — берёт `window.location.origin`
- Seed-пользователи вставлены через SQL с `emailVerified = true`
---
## Команды
```bash
# Разработка
npm run dev # localhost:3000
docker compose up -d # Поднять PostgreSQL локально
# Проверка качества
npm run lint # ESLint
npm run type-check # tsc --noEmit
# Сборка
npm run build
npm run start
# База данных
npx prisma migrate dev --name <snake_case_name> # Новая миграция
npx prisma migrate deploy # Применить в production
npx prisma generate # Пересоздать клиент
npx prisma db seed # Заполнить тестовыми данными
npx prisma studio # GUI для БД
# Production-деплой (на сервере в /root/digital-household/lms-sb/)
git pull
docker compose -f docker-compose.prod.yml up -d --build
```
При старте production-контейнер автоматически запускает `prisma migrate deploy`, затем `node server.js`.
---
## Структура проекта
```
lms-system/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── (auth)/ # login, register, verify-email
│ │ ├── (student)/ # dashboard, courses/[slug], lessons/[lessonId]
│ │ ├── curator/ # homework review, dashboard
│ │ ├── admin/ # courses, users, settings, categories
│ │ └── api/ # REST endpoints + Better Auth handler
│ ├── components/
│ │ ├── ui/ # shadcn/ui (автогенерация, не трогать)
│ │ ├── editor/ # TipTap WYSIWYG
│ │ ├── player/ # Kinescope Player wrapper
│ │ ├── course/ # Компоненты курса
│ │ └── layout/ # Header, Sidebar, Footer
│ ├── lib/
│ │ ├── auth.ts # Better Auth config (сервер)
│ │ ├── auth-client.ts # Better Auth client (браузер)
│ │ ├── prisma.ts # Prisma singleton
│ │ ├── s3.ts # Hetzner S3 клиент
│ │ ├── email.ts # Resend email helpers
│ │ └── utils.ts # cn() и утилиты
│ ├── types/ # TypeScript-типы
│ └── middleware.ts # Auth middleware (защита маршрутов) + PUBLIC_ROUTES
├── prisma/
│ ├── schema.prisma # Схема БД (~314 строк)
│ ├── seed.ts # Тестовые данные
│ └── migrations/ # НЕ РЕДАКТИРОВАТЬ ВРУЧНУЮ
├── docker-compose.yml # Локальная разработка
├── docker-compose.prod.yml # Production
├── Dockerfile # Multi-stage build
├── .env.example # Шаблон переменных (без секретов)
└── .env.local # Локальные секреты (в .gitignore)
```
---
## Роли и маршруты
| Роль | Маршруты | Описание |
|------|---------|----------|
| `admin` | `/admin/*`, `/curator/*`, всё | Полный доступ |
| `curator` | `/curator/*`, `/dashboard` | Проверка ДЗ, комментарии |
| `student` | `/dashboard`, `/courses/*` | Просмотр курсов, прогресс |
Защита маршрутов — в `src/middleware.ts` (массив `PUBLIC_ROUTES`, сверка по `startsWith`) + проверка сессии в layout/page.
---
## Модель данных (ключевые сущности)
```
User → Session, Account, Verification # Better Auth
Category → Course → Module → Lesson # Структура контента
Lesson → LessonFile # Файлы к уроку
CourseEnrollment (userId + courseId) # Доступ с expiresAt
AccessLog # Аудит доступов
LessonProgress (userId + lessonId) # Прогресс ученика
Lesson → Homework → HomeworkSubmission → HomeworkFeedback # ДЗ
Lesson → LessonComment # Обсуждения (soft-delete)
Lesson → Quiz → QuizQuestion → QuizOption # Тесты
Quiz → QuizAttempt # Результаты тестов
Settings (key-value) # Настройки платформы
```
---
## Дизайн-система «Second Brain Aubade»
Типографский, монохромный, газетный стиль.
| Токен | Значение |
|-------|---------|
| Шрифт | Fira Mono (400/500/700, Latin + Cyrillic) |
| Фон | `#F5F5F0` (тёплый off-white) |
| Текст | `#323232` |
| Поверхность | `#E8E8E0` |
| Акцент | `#E8F0D8` (зелёный) |
| Border | `#AAAAAA` |
| Сайдбар | `#2A2A28` (тёмный) |
**Aubade-эффект** (карточки и кнопки):
- Border: `2px solid #AAAAAA`
- Shadow: `4px 4px 0 0 #AAAAAA`
- Hover: `translate(-2px, -2px)` + shadow `6px 6px`
- Active: `translate(2px, 2px)`, shadow убирается
- CSS-классы: `.card-aubade`, `.btn-aubade`, `.btn-aubade-accent`, `.tag-aubade`
---
## Инфраструктура
| Компонент | Значение |
|-----------|---------|
| Сервер | Hetzner VPS — 8 vCPU / 16 GB RAM / 320 GB NVMe |
| Reverse proxy | Caddy (auto HTTPS, Let's Encrypt) |
| Порт | 3010 (внутри контейнера 3000) |
| БД | PostgreSQL 16 (контейнер `lms-sb-db-1`) |
| Object Storage | Hetzner S3, endpoint `nbg1.your-objectstorage.com`, бакет `second-brain-lms` |
| Git | Gitea — `https://git.second-brain.ru/admins/lms-sb` |
| Email | Resend, домен `mailsend.second-brain.ru` |
| Бэкапы | PostgreSQL → Backblaze B2 (ежедневно, 03:00, ротация 7 дней) |
---
## Переменные окружения
```env
DATABASE_URL="postgresql://lms_user:password@localhost:5432/lms_db"
BETTER_AUTH_SECRET="generate-with-openssl-rand-base64-32"
BETTER_AUTH_URL="http://localhost:3000"
RESEND_API_KEY=""
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"
```
Секреты — **только в `.env.local`**. При добавлении новых переменных обновлять `.env.example`.
---
## Правила написания кода
### Языки
- **UI-строки** (заголовки, кнопки, сообщения): русский
- **Переменные, функции, файлы, комментарии**: английский
- **Коммиты**: английский, imperative mood (`Add lesson progress`, `Fix auth redirect`)
### Стиль
- Server Actions для форм и мутаций
- Не добавлять абстракции «на будущее» — только текущий этап
- Нет `console.log` в production (только `console.error` для реальных ошибок)
- Нет захардкоженных секретов, URL, ID
### Миграции БД
- **Никогда** не редактировать `prisma/migrations/` вручную
- **Всегда** спрашивать перед миграцией, которая меняет или удаляет существующие поля
- Имена миграций: английский, snake_case (`add_lesson_progress`)
- Перед `prisma migrate deploy` на production — бэкап БД
### Файлы и загрузки
- Все файлы (ДЗ, PDF, изображения) — через Hetzner Object Storage, **не на диск VPS**
- Обложки курсов: 16:9, max 5 MB, JPG/PNG/WebP
- Изображения в уроках: max 10 MB
- Файлы к уроку: max 100 MB, PDF/ZIP/DOCX
### Коммиты
- Один коммит = одна логически завершённая единица
- Перед коммитом: `npm run lint && npm run type-check`
- После завершения каждого этапа ROADMAP — `git push` в Gitea
---
## Чек-лист перед коммитом
- [ ] `npm run lint` — без ошибок
- [ ] `npm run type-check` — без ошибок
- [ ] Новые `.env` переменные добавлены в `.env.example`
- [ ] Миграция БД согласована (если есть)
- [ ] Нет `console.log`, нет секретов в коде
---
## Тестовые аккаунты
| Email | Пароль | Роль |
|-------|--------|------|
| admin@second-brain.ru | Password123! | admin |
| curator@second-brain.ru | Password123! | curator |
| student@second-brain.ru | Password123! | student |
---
## Текущий статус проекта
**Завершено (9 из 13 этапов):**
- Этап 0: Каркас, auth, роли, деплой
- Этап 1: Курсы → Модули → Уроки (CRUD, drag-and-drop, TipTap, S3)
- Этап 1.5: Расширенный доступ (сроки, категории, AccessLog)
- Этап 2: Kinescope-интеграция, рендер уроков для ученика
- Этап 3: Прогресс (кнопка завершения, прогресс-бар)
- Этап 5: Домашние задания + обратная связь куратора
- Этап 6: Обсуждения под уроками
- Этап 7: Email-уведомления (Resend)
- Этап 8: Импорт уроков из Markdown (Obsidian)
**В работе:**
- Этап 9: Настройки платформы (Admin Settings)
**Впереди:**
- Этап 11: Импорт/экспорт учеников (CSV, миграция с emdesell)
- Этап 12: Telegram-бот + аналитика (Yandex.Metrika)
- Этап 13: Тесты и квизы с автопроверкой
**Бэклог:** сертификаты, геймификация, платежи, медиатека, цифровой сад, CI/CD
Полный роадмап с деталями и критериями готовности — в `ROADMAP.md`.
---
## Известные ограничения
- Seed-пользователи вставлены через SQL с `emailVerified = true` (обход Better Auth)
- Загрузка файлов: нет лимита на уровне Next.js (только S3)
- Drag-and-drop: возможны race conditions при быстрых перетаскиваниях (некритично)
- `expiresAt` проверяется в UI, но не блокирует доступ на уровне middleware