POST /api/internal/quiz-lead accepts archetype quiz results and
idempotently upserts subscriber in Listmonk (create on 201, PUT on 409).
Documents QUIZ_LEAD_SECRET and LISTMONK_* vars in .env.example.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Attachments uploaded via question-upload get a CDN URL
(files.second-brain.ru via S3_CDN_URL), but the validator only accepted
the direct S3 endpoint prefix — so every attachment was silently dropped
(message text saved, file lost). Add isAllowedPublicUrl (CDN + direct S3)
in lib/s3 and use it in both question routes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Curators/admins can mark a submission APPROVED (and complete the lesson)
without writing feedback. The student sees 'ДЗ принято ✓' and can no
longer resubmit. No email is sent on silent approval.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mustChangePassword guard redirected an impersonating admin to
/change-password, which sits outside the student layout (no
return-to-admin banner) — a dead end. Skip the guard when impersonating,
and add the stop-impersonate banner to the change-password page as a
fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
getPublicUrl returns the CDN URL (files.second-brain.ru via Bunny) when
S3_CDN_URL is configured, falling back to the direct S3 endpoint. This
was a manual patch on the build server that was never committed —
restore it so new uploads also get CDN-backed (RU-accessible) URLs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lessons with homework/quiz have no manual complete button — they
auto-complete on submission. Before submission nothing was shown,
leaving students unsure how to progress. Add a muted hint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lesson files were served inline (no Content-Disposition) → browsers
opened PDFs in a new tab instead of downloading; flaky across
browsers/providers. Set Content-Disposition: attachment with the
human-readable filename (RFC 5987 for Cyrillic) on upload.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- /api/internal/grant: secret-auth endpoint for payment-router to
provision access on a paid order — find-or-create user (emailVerified,
temp password, mustChangePassword), enroll by course slug list,
AccessLog, delivery email. Idempotent by order_id.
- User.mustChangePassword flag (+ migration); (student) layout redirects
flagged users to /change-password (forced first-login change).
- email.ts: sendCourseGrantEmail (temp password for new buyers).
- middleware: /api/internal is public (own secret auth).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Block direct /api/auth/sign-up at the middleware (404): it bypassed
the Turnstile/honeypot wrapper. /api/register is unaffected — it
invokes auth.handler programmatically, not through HTTP.
- Tighten rate limits on send-verification-email and forget-password
(3/min/IP): both send emails to arbitrary addresses and shared the
loose global 100/min limit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Better Auth does not auto-resend on 403 (verified against Resend logs),
so request a fresh link via authClient.sendVerificationEmail before
telling the user a new email was sent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
requireEmailVerification was enabled but no sendVerificationEmail was
configured — new users saw "check your email", never received anything,
and couldn't log in (403 shown as "wrong email or password").
- email.ts: add verification email template; welcome email no longer
claims the account is "confirmed"
- auth.ts: configure emailVerification (send on signup, resend on
unverified login attempt, auto sign-in after verification, 24h TTL)
- register route: callbackURL=/dashboard so the verify link lands in
the cabinet
- login form: distinguish 403 (unverified — tell user a fresh link was
sent) and 429 (rate limit) from wrong credentials
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Turnstile/honeypot wrapper /api/register was missing from
PUBLIC_ROUTES, so unauthenticated POSTs were redirected to /login (307).
The register form followed the redirect, got HTML, and failed to parse
it — surfacing as "connection error". This silently broke signups for
new users. Add /api/register alongside /api/auth.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Backend: explicitly return 409 EMAIL_TAKEN when the email already
exists, instead of Better Auth's silent 200 (common after migration).
- Frontend: split error handling — network failure, non-JSON body
(502/proxy), and meaningful server errors get distinct messages; the
"email already registered" case shows login / reset-password links
instead of a generic "connection error".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract ImpersonateButton into a shared component and reuse it on the
admin user profile page (header, hidden for admins) — same behaviour as
the users list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Quick-enroll modal now takes access duration in days (0/empty = unlimited)
and shows the computed expiry date, instead of a manual date picker.
- Dashboard upsell no longer shows the "Всё включено" bundle of a course
the student already owns in its base edition (obsidian/zotero).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The admin shell sidebar was always fixed at 208px with no mobile
handling, so on phones it covered half the screen with no way to hide
it. Add a hamburger toggle + overlay (mirroring the student course
sidebar): off-canvas on <lg, sticky as before on desktop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Show the sellable course lineup on the dashboard: courses the student
doesn't own render as blurred cards with a lock and a CTA linking to the
product landing page (new tab). Owned courses are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kinescope renders an iframe that ignores z-index stacking.
Switch to mutually-exclusive render: show poster image OR
the player, never both. Click on poster dismisses it and
reveals the player.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Kinescope React player ignores the poster prop. Replace with a
click-to-dismiss overlay rendered above the player: shows the
lesson coverImage, a play button, and on click hides itself
and calls player.play() via ref.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- KinescopeVideo TipTap node: insert video anywhere in lesson content via toolbar button; editor shows ID preview, student view renders KinescopePlayer
- ImageWithControls NodeView: hover over any image in editor to reveal Replace and Delete buttons
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Staging runs at staging.school.second-brain.ru on Hetzner port 3011.
Deploy script: ~/Documents/Claude/scripts/deploy-staging.sh
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add admin auth guard (redirect to /dashboard if not admin)
- Add delete-action.ts with deleteComment(commentId) soft-delete action
- Fix pagination label to show "Страница X из Y · Всего: N"
- Existing actions.ts, comments-table.tsx, and admin-nav entry preserved
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace client HomeworkFilters component with server-side <form method="GET">
- Switch search param from `search` to `q`, add lesson title to search scope
- Change status filter from DB status field to feedback-count logic (pending=no feedbacks, reviewed=has feedbacks)
- Update pagination label to "Страница X из Y · Всего: N"
- Preserve all existing submission links and layout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a "+ Доступ" button to each user row in the admin users table.
Clicking it opens a centered modal with a course dropdown, optional
expiry date, and a Server Action that upserts CourseEnrollment, logs
to AccessLog, and sends sendCourseAccessEmail on new enrollments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add emailVerified filter (true/false/any) to UsersSearch component
- Wire emailVerified param through page searchParams, where clause, and pageUrl helper
- Preserve emailVerified in pagination links alongside existing search/role/balance params
- Update pagination label to "Страница X из Y · Всего: N"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace non-existent --surface/--surface-muted/--border-strong with actual
design-system tokens (--color-surface, --background, --foreground, --muted)
- Remove tmp/ segment from S3 upload key in question-upload route
- Add auto-scroll to bottom on new message in QuestionThread
- Block Send while file upload is in progress (uploading guard)
- Replace <a> with Next.js <Link> in new question page back-link
- Replace hardcoded #c00 error color with var(--destructive) in both files
- Replace hardcoded #E8E8E0/#F5F5F0 hex backgrounds with CSS tokens
Tasks 8–10: student-facing questions UI — list page with unread badges,
new question form with client-side submission, and thread page with
QuestionThread component for real-time reply + file attachment flow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add CLOSED status guard in messages POST (returns 409)
- Add extension allowlist check in upload route + text/x-markdown MIME type
- Sanitize files JSON array before DB write
- Add sendQuestionFollowUpEmail helper and use it for student follow-up replies
- Scope email field to staff only in questions list query
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements GET/POST /api/questions, GET /api/questions/[id] with read tracking, POST /api/questions/[id]/messages with email notifications, PATCH /api/questions/[id]/close for staff, and POST /api/student/question-upload for file attachments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Прежний DESIGN.md ссылался на легаси-дизайн-систему v1 (кремовая
палитра + лаванда) и расходился с реальным globals.css. Новый —
указатель на канон ДС-2 «Second Brain LMS & Press» (терминальный
Aubade) и описание того, где токены живут в этом репозитории.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Переопределены токены шкалы Tailwind (--text-xs…--text-5xl) на +2px,
базовый размер body 18px, размеры компонентных классов (.btn-aubade,
.tag-aubade, .admin-sidebar-nav-link) и инлайновые fontSize приведены
к канону дизайн-системы ДС-2. Rem-база (html 16px) не тронута —
спейсинг и сетка не затронуты, растёт только текст.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hamburger button (top-left, lg:hidden), dark overlay, slide-in animation.
Sidebar closes on lesson link click. Spacer added to prevent content overlap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces client-side fetch with a proper Server Action + useActionState.
Uncontrolled inputs fix agent-browser testing and improve reliability.
Server action verifies bcrypt hash directly via Prisma.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
authClient.changePassword does not exist in better-auth v1.6 client bundle.
Use direct POST to /api/auth/change-password endpoint instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New page /profile: shows name/email and password change form
- Uses authClient.changePassword (current + new + confirm)
- Student name in header is now a link to /profile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tw-animate-css, shadcn, @tailwindcss/typography were removed in F008
but their @import/@plugin lines remained in globals.css, breaking the build.
CSS variables are defined inline so none of these imports are needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users can now request a password reset link via email. Better Auth
sendResetPassword callback sends a branded email via Resend. Login
page shows success notice after password is set.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces BalanceTransaction model to track per-user balance history
(prepayments, refunds, partner credits). Admin can add/delete transactions;
current balance is computed as the running sum.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests login → dashboard → course page → 3 random lessons flow.
VU-level session: each VU logs in once and reuses the cookie jar.
Thresholds: p(95) < 3s, error rate < 5%.
Results on 50 VU / 5 min: p(95)=329ms, errors=4.94% (login rate-limit retries).
The @kinescope/react-kinescope-player library accesses browser APIs
(window/document) during server-side rendering. In Next.js App Router,
client components are SSR-rendered on full page loads (direct URL,
refresh) but not on RSC navigations. This caused a 500 error for all
lessons with a kinescopeId when accessed directly.
Fix: defer rendering KinescopeReactPlayer until after mount with
useEffect + useState(false), so it only runs in the browser.
- Add coverImage field to Lesson model (prisma)
- Pass coverImage as poster prop to KinescopePlayer
- Show quiz in read-only preview mode for admin on lesson page
- Fix TipTap v3 editor reset on save: pass [lesson.id] as deps to useEditor
to prevent setOptions() from reinitializing content on every re-render
- Replace saveLesson Server Action call with fetch PATCH /api/admin/lessons/[id]
to avoid Next.js 16 automatic RSC refresh after Server Actions
- Simplify revalidatePath: only revalidate module page, not lesson editor page
- Add parentId field to LessonComment (self-referential FK, SetNull on delete)
- Show replies indented under parent comment with left border visual
- Add reply button (visible to admin/curator only) with inline textarea
- Only root comments shown in main list; replies nested below their parent
- Update comment count to include replies
- Server-side validation: only admin/curator can reply, parent must belong to same lesson
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /api/admin/lesson-files now checks for an existing record with the
same (lessonId, name) before uploading — replaces it (old S3 object
deleted) instead of always creating a new one. Previously every save
cycle accumulated an extra copy; 1183 duplicates occupying 6.5 GiB were
found and cleaned up.
DELETE now receives the file URL and extracts the S3 key from it, so
manual deletion actually removes the object from storage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add phone/birthday columns via Prisma migration
- Admin user page shows phone and birthday with inline edit UI
- UserContactEditor client component for editing contact info
- updateUserContact server action with admin-only guard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wire settings to actual platform behavior: maintenance mode, registration toggle,
notification emails, curator feedback emails, email verification flag
- Add logo (logoUrl, showLogo) and social network links (YouTube, VK, Telegram) settings
- Show logo + school name dynamically in student layout header
- Add footer to student layout with org requisites and social links
- Register page: read settings server-side, validate terms checkbox with legal links
- Login page: show notice when redirected from closed registration
- Settings form: add Logo and Social Networks sections
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- /admin/quizzes: list all quizzes with question and attempt counts
- /admin/quizzes/[quizId]: view all student attempts with answers per question
- Add "Тесты" link to admin sidebar navigation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Show "Отметить как пройденный" button only on lessons without homework
- Show static "Пройдено" badge on homework lessons completed via approval
- Auto-create LessonProgress when curator/admin approves homework submission
- Revalidate student lesson, course, and dashboard pages on approval
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Store human-readable label in LessonFile.name via optional label field on upload
- Add PATCH endpoint to rename existing files inline
- Admin: label input before upload, click-to-edit inline rename
- Student: colored format badge (PDF/DOCX/XLSX/ZIP/etc) replaces paperclip emoji
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>