Compare commits

150 Commits

Author SHA1 Message Date
admins a8cd2cd4d4 feat: auto-enroll new signups into free course obsidian-start (freemium)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:05:52 +05:00
admins 2ea9c6fda0 Don't force password change while admin is impersonating
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>
2026-06-17 11:03:58 +05:00
admins ec011ab7fd Add support mailto on login for missing password-reset email
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 12:58:28 +05:00
admins 27027b134f Use CDN URL for public file links when S3_CDN_URL is set
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>
2026-06-16 12:46:31 +05:00
admins 04e8377dab Show hint on lessons with pending homework or quiz
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>
2026-06-16 12:22:44 +05:00
admins 5d4630519c Force download for lesson materials (Content-Disposition: attachment)
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>
2026-06-15 16:06:51 +05:00
admins a7abf454d9 Add internal grant endpoint + temp-password / force-change flow
- /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>
2026-06-14 16:25:35 +05:00
admins 5a67ac8086 Close captcha-bypass and email-abuse vectors
- 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>
2026-06-12 14:18:30 +05:00
admins 85cf8be705 Explicitly resend verification email on unverified login
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>
2026-06-12 14:05:32 +05:00
admins 2790a7d24b Wire up email verification flow for new signups
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>
2026-06-12 14:01:46 +05:00
admins 619ae393bd Allow /api/register through auth middleware
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>
2026-06-12 13:26:19 +05:00
admins 8240db5d22 Make registration errors informative
- 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>
2026-06-12 13:20:56 +05:00
admins 05f46ec830 Add "login as user" to user profile card
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>
2026-06-09 15:55:57 +05:00
admins 3b8a1ce7ee Link question author name to their admin profile
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:53:01 +05:00
admins a290b9495c Enroll modal: days input instead of date; hide bundle if base course owned
- 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>
2026-06-08 16:13:15 +05:00
admins 8c9f1e0a55 Add mobile spacer so admin content clears the hamburger button
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 10:01:58 +05:00
admins ffc5905309 Make admin/curator sidebar collapsible on mobile
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>
2026-06-08 09:58:15 +05:00
admins 3ed5540f40 Reduce blur on locked course cards for readability
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:06:32 +05:00
admins 7dc3412903 Add locked course cards (upsell) to student dashboard
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>
2026-06-06 16:01:39 +05:00
admins 7c85a7835b Widen dashboard course cards and enlarge title/description text 2026-06-04 10:32:30 +05:00
admins ecf48de7e8 Copy code block verbatim (fences live in content for Obsidian snippets) 2026-06-03 15:34:15 +05:00
admins ecbca89cef Copy full markdown fence for language-tagged code blocks (dataview/query) 2026-06-03 15:23:27 +05:00
admins cf942d223e Mark lesson complete on homework submit; add copy button + styling to code blocks 2026-06-03 14:58:30 +05:00
admins adc1d6fc06 Loosen sign-in rate limit to 10/min per IP for migration onboarding 2026-06-03 14:03:11 +05:00
admins 6624a0c535 Restore list markers (disc/decimal) for lesson content lists 2026-06-03 13:34:11 +05:00
admins a65125a239 Linkify URLs and preserve line breaks in question messages 2026-05-27 14:16:25 +05:00
admins 14b1eacf0d Switch homework pending filter from feedbacks-none to status-based (PENDING/REVIEWING) 2026-05-27 11:59:25 +05:00
admins d9e3531c49 Apply lesson cover as poster for embedded kinescopeVideo nodes 2026-05-26 16:40:47 +05:00
admins 5e5f616888 debug: move banner outside kinescopeId block to catch null ID 2026-05-26 16:22:18 +05:00
admins 4222834539 temp: visible debug banner for coverImage 2026-05-26 16:11:33 +05:00
admins 2525ca2b32 temp: debug coverImage value in lesson page 2026-05-26 15:49:47 +05:00
admins cfd3681979 Fix poster overlay hidden behind Kinescope iframe
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>
2026-05-26 15:40:23 +05:00
admins c2c2190cb6 Show custom lesson cover as video poster overlay
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>
2026-05-26 15:35:39 +05:00
admins 6542f9b6b6 Add NEXT_PUBLIC_TURNSTILE_SITE_KEY build arg to Dockerfile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 12:39:46 +05:00
admins 231ed083be Add anti-bot registration protection
- Custom /api/register route: honeypot, disposable email blocking, Cloudflare Turnstile verification
- Register form: honeypot hidden field, Turnstile widget (loads when NEXT_PUBLIC_TURNSTILE_SITE_KEY set)
- Install disposable-email-domains package
- Add NEXT_PUBLIC_TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY env vars to .env.example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 12:35:06 +05:00
admins c1c33a0619 Make links in lesson content visually distinct (underline + bold)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:35:07 +05:00
admins 7e6ad8067c Add in-content video embed and image replace/delete controls
- 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>
2026-05-26 11:27:52 +05:00
admins 287347168d Add cross-module lesson navigation and admin edit button
- Admin lesson editor now navigates across module boundaries (full course flat list)
- Student lesson view shows "Edit" button for admins linking to lesson editor

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 11:05:46 +05:00
admins f3428420e2 Add migration for Session.impersonatedBy column 2026-05-25 16:01:57 +05:00
admins 38267fa432 Add staging environment plan and deploy script
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>
2026-05-25 15:13:26 +05:00
admins 30405e768b Show last login date on admin user detail page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 09:37:10 +05:00
admins 3b57b14d9b Add file attachments to questions (new question form + admin reply)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 18:26:17 +05:00
admins 367764b71e Expand /questions/new form: wider container, larger inputs, hint texts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 18:10:33 +05:00
admins acf7ee49aa Replace single-line reply input with resizable textarea in QuestionSplitView 2026-05-19 18:01:27 +05:00
admins 751c012f3d Add /admin/comments page with delete and pagination
- 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>
2026-05-19 14:31:38 +05:00
admins 7084806aac Add search, filters, pagination to homework list
- 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>
2026-05-19 14:29:47 +05:00
admins b2fa98051f Add quick enroll button to admin users table
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>
2026-05-19 14:27:57 +05:00
admins 4f5b5c535a Add search, filters, pagination to admin users table
- 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>
2026-05-19 14:25:53 +05:00
admins e5ba94cb33 Fix security, transaction, and badge issues from final review
- Validate file URLs against S3 prefix in messages route (Fix 1)
- Guard attachment hrefs with https:// check in QuestionThread and QuestionSplitView (Fix 2)
- Wrap message create + updatedAt bump in prisma.$transaction (Fix 3)
- Add questionsBadge count query to curator layout for admin branch (Fix 4)
- Fire-and-forget email sends with void Promise.all (Fix 5)
- Wrap req.json() calls in try/catch returning 400 on parse failure (Fix 6)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:56:31 +05:00
admins 12e1785ff2 Send homework-updated email to staff on submission edit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:52:04 +05:00
admins bd1e77c2a3 Add questions nav links and admin unread badge
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:49:18 +05:00
admins d2362a3f1e Fix QuestionSplitView error handling, loading state, file key
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:47:35 +05:00
admins 3a2f64d47d Fix QuestionSplitView panel widths and message bubble styling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:45:57 +05:00
admins d32186c101 Add admin/curator split-view questions page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:42:39 +05:00
admins f7d428180b Fix student questions pages: CSS tokens, scroll, upload guard, S3 path
- 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
2026-05-19 13:40:05 +05:00
admins c5d2caa345 Fix message alignment in QuestionThread 2026-05-19 13:32:52 +05:00
admins 89d614fa00 Add student questions list, new question form, and thread pages
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>
2026-05-19 13:31:31 +05:00
admins a9e6272d2d Fix API routes: closed-question guard, file validation, files sanitization, follow-up email
- 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>
2026-05-19 13:28:08 +05:00
admins f2946db57a Add student questions API routes
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>
2026-05-19 13:23:26 +05:00
admins 9cb56b9b04 Add question and homework-update email helpers 2026-05-19 13:20:06 +05:00
admins 6fa49d4113 Add indexes to StudentQuestion and StudentQuestionMessage 2026-05-19 13:18:54 +05:00
admins 90f155d334 Add StudentQuestion and StudentQuestionMessage models
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:11:51 +05:00
admins d47f79be1a Add student questions implementation plan 2026-05-19 12:59:25 +05:00
admins ec128f670a Add student questions feature design spec 2026-05-19 12:52:57 +05:00
admins a27089bc0c docs(lms): заменить устаревший DESIGN.md указателем на ДС-2
Прежний DESIGN.md ссылался на легаси-дизайн-систему v1 (кремовая
палитра + лаванда) и расходился с реальным globals.css. Новый —
указатель на канон ДС-2 «Second Brain LMS & Press» (терминальный
Aubade) и описание того, где токены живут в этом репозитории.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 17:14:16 +05:00
admins c94a8dafa9 style(lms): синхронизировать типографику со шкалой ДС-2 (+2px)
Переопределены токены шкалы 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>
2026-05-18 17:14:16 +05:00
admins 47840901c5 feat: collapsible mobile sidebar for student course view
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>
2026-05-14 05:07:29 +00:00
admins e3e6c713d2 Add reset password button to admin user page 2026-05-11 19:34:33 +05:00
admins 77016a03c7 Add active users last 24h card to admin dashboard 2026-05-11 18:52:40 +05:00
admins c1ae048c14 Rewrite password change form to use Server Action
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>
2026-05-11 17:41:42 +05:00
admins 799117d287 Fix change-password form to use direct fetch instead of authClient
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>
2026-05-11 17:10:18 +05:00
admins c445bfacad Add student profile page with password change
- 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>
2026-05-11 17:03:33 +05:00
admins 41871a1e8e Fix build: remove deleted package imports from globals.css
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>
2026-05-11 16:56:06 +05:00
admins 444b9c0faf Apply tech debt fixes: middleware rename, React.cache, file size limits, remove dead deps
- Rename proxy.ts → middleware.ts, export proxy() → middleware() so Next.js edge
  protection actually activates (F001)
- Add PUBLIC_ROUTES entries for /forgot-password and /reset-password
- Wrap getSettings() in React.cache() to eliminate duplicate DB call in root layout (F003)
- Remove 4 console.log calls from saveLesson Server Action, keep console.error (F005)
- Add 50 MB file size guard to all 6 upload routes before arrayBuffer() read (F004)
- Remove unused deps: @tailwindcss/typography, shadcn, tw-animate-css (F008)
- Update CLAUDE.md: Prisma version 6.x → 7.x
- Add TECH_DEBT_AUDIT.md with 14 findings across 9 dimensions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:35:05 +05:00
admins 5547b427bb Add nonzero balance filter to users page, link from dashboard card 2026-05-08 14:24:31 +05:00
admins 2dfc42821c Move balance card below Activity block in admin dashboard 2026-05-08 14:20:44 +05:00
admins 33dcf9bb4a Add total user balances stat card to admin dashboard 2026-05-08 14:09:59 +05:00
admins a5e7b20699 Add forgot-password and reset-password flow
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>
2026-05-07 11:04:06 +05:00
admins 93e74951a7 Add balance transactions to user admin panel
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>
2026-05-07 09:24:25 +05:00
admins 48721759d3 Add comment field to user profile in admin panel
- Prisma: User.comment String? column + migration
- UserContactEditor: comment shown in view mode, textarea in edit mode
- updateUserContact action: saves comment to DB
2026-05-06 14:06:53 +00:00
admins 4f3b389f05 Update load test to 100 VUs with login jitter
Stages: 10 → 50 → 100 VUs, hold 3 min, ramp down.
Added random 0-10s sleep before first login to spread auth requests
and reduce Better Auth rate-limit retries.
Results: p(95)=244ms, pages 100% OK, auth retries 6.28% (rate-limit artifact).
2026-05-06 12:33:49 +00:00
admins 628226151b Add k6 load test script for 50 concurrent users
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).
2026-05-06 11:51:39 +00:00
admins 9a21c705b7 Fix KinescopePlayer SSR crash on direct page load
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.
2026-05-06 11:42:01 +00:00
admins 7888a7598b Add coverImage poster to player, fix TipTap v3 editor reset, quiz admin preview
- 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
2026-05-01 13:26:30 +00:00
admins c25369b766 Add threaded comment replies for admin and curator
- 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>
2026-04-29 13:17:30 +05:00
admins 6b5bfc853e Add name/email editing and days-based course access in admin user card
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 12:01:01 +05:00
admins e691124058 Fix LessonFile duplication: upsert on upload, delete S3 on remove
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>
2026-04-28 10:59:37 +05:00
admins fdb9f96382 Add phone and birthday fields to User model with admin editor
- 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>
2026-04-27 16:43:35 +05:00
admins c64f393a7b Implement platform settings (Stage 9)
- 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>
2026-04-27 15:31:10 +05:00
admins ba0a630fd9 Fix quiz attempts page: fetch users separately (no User relation on QuizAttempt)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 12:07:28 +05:00
admins 2468671d82 Fix QuizAttempt field name: createdAt -> completedAt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 12:06:13 +05:00
admins 7242a989ba Add admin quiz attempts viewer
- /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>
2026-04-27 12:05:01 +05:00
admins d2150153df Add quiz feature: student UI, admin editor, lesson page integration
- QuizSection component: shows questions as text inputs, read-only after submission
- QuizEditor component: admin CRUD for quiz questions with type selector
- saveQuiz/deleteQuiz server actions for admin
- submitQuizAttempt server action: idempotent, auto-marks lesson complete
- Student lesson page: renders QuizSection, updates complete button logic
- Admin lesson page: renders QuizEditor below homework section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 11:43:16 +05:00
admins 3ed7bc147b Add lesson complete button with homework-aware logic
- 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>
2026-04-26 14:00:47 +05:00
admins 39d84a3db2 Add labeled file materials with format badge
- 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>
2026-04-26 11:55:07 +05:00
admins 15df731e37 Make lesson editor header and toolbar sticky on scroll
Both the controls bar (Save button, publish toggle) and the formatting
toolbar now stick to the top of the viewport while editing long lessons.
Header sticks at top: 0, toolbar sticks just below it at top: 62px.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 18:10:07 +05:00
admins bfa037885f Fix saveLesson: sanitize content JSON to prevent RSC proxy error
React treats TipTap's editor.getJSON() output as a non-plain object when
passed through Server Action serialization, causing isDecimal() inside
Prisma's query builder to receive a temporary client reference proxy and
throw ERROR 1213974697. JSON.parse(JSON.stringify()) strips the prototype
chain and any non-enumerable properties, ensuring Prisma receives a
clean plain object.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 17:58:13 +05:00
admins 8757537344 Debug: add try/catch and JSON sanitize in saveLesson
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 17:54:10 +05:00
admins 65aa669522 Fix prisma generator provider to prisma-client
The generated TypeScript client in src/generated/prisma was created with
prisma-client provider (new TypeScript-first generator), but schema.prisma
had prisma-client-js. This caused Docker builds to generate files in
node_modules/@prisma/client instead of src/generated/prisma, breaking the
@/generated/prisma/client import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 17:47:26 +05:00
admins f4e74b38d4 Add explicit prisma output path to fix Docker build
Without output directive, Prisma 7 generates to node_modules/@prisma/client
in the Docker build context, causing the @/generated/prisma/client import
to fail with "Module not found". Explicit output ensures generated TypeScript
files are always placed at src/generated/prisma/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 17:38:19 +05:00
admins c050c005e4 Include src/generated in Docker build context for Prisma 7 TS client
Remove src/generated from .dockerignore so Turbopack can resolve
@/generated/prisma/client during build. The files are regenerated
by prisma generate inside the builder anyway.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 17:29:47 +05:00
admins af1fb6f61e Fix RSC toStringTag error: import PrismaClient from generated TS client
Use @/generated/prisma/client instead of @prisma/client to avoid
Turbopack creating a broken external proxy for the missing
.prisma/client/default module at runtime. Add @prisma/adapter-pg
to serverExternalPackages, remove unused resolveAlias.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:08:09 +05:00
admins 09e5653191 Add Turbopack resolveAlias for Prisma client to fix RSC crash
Without resolveAlias, Turbopack fails to resolve .prisma/client/default
and creates a temporary client reference, causing the toStringTag error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 14:35:26 +05:00
admins 29f6533e63 Switch to prisma-client-js generator to fix Turbopack RSC crash
The new prisma-client generator outputs TypeScript files to src/generated/prisma/
which include import.meta.url at module level. Turbopack sees this and marks the
entire module as a client reference, causing 'Cannot access toStringTag on the
server' on every page that uses Prisma.

Switching to prisma-client-js puts the generated client in node_modules/@prisma/client
where serverExternalPackages can properly exclude it from the server bundle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 14:32:31 +05:00
admins 4821764a4f Fix Prisma 7 + Turbopack RSC compatibility by adding serverExternalPackages
Next.js 16 with Turbopack bundles @prisma/client into the RSC bundle,
causing it to be treated as a client module and creating 'temporary client
references'. This triggers the 'Cannot access toStringTag on the server' error
whenever Prisma result objects are used in Server Components.

Adding serverExternalPackages tells Turbopack to treat these as native
Node.js packages and keep them out of the RSC bundle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 14:23:02 +05:00
admins 5dfa79d357 Fix all Server Actions imported from dynamic route paths
All admin and student Client Components were importing Server Actions
from paths with dynamic segments ([courseId], [moduleId], [lessonId], [slug]).
This caused "Cannot access toStringTag on the server" RSC crash.

Consolidated all Server Actions into static files under src/lib/actions/:
- course-actions.ts  (modules + enrollment)
- module-actions.ts  (lessons + reorder + move)
- user-actions.ts    (bulk grant / revoke)
- student-actions.ts (progress + homework + comments)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 14:05:26 +05:00
admins 9eb21e3ab4 Move Server Actions to static paths to fix RSC temporary client reference error
Server Actions imported from dynamic route paths ([courseId]/[moduleId]/[lessonId])
caused "Cannot access toStringTag on the server" crash after saving a lesson.
Moved saveLesson, saveHomework, deleteHomework to src/lib/actions/*.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:58:27 +05:00
admins af8644ebce Serialize all Prisma proxy data in admin lesson and module pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:48:43 +05:00
admins 0bde11b86e Serialize all Prisma data before passing to Client Components
Prisma 7 proxy objects (DateTime, _count, relations) cannot be
serialized by React RSC. Convert all course page data to plain
JSON objects with JSON.parse/stringify before passing as props.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:42:05 +05:00
admins d8be6d6d95 Fix Prisma 7 JSON proxy serialization in RSC props
Prisma 7 wraps Json fields in proxy objects that RSC cannot serialize.
Fix: select specific columns (exclude content) in module page,
and JSON.parse/stringify lesson content before passing to client.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:36:04 +05:00
admins 9731fcab48 Add impersonatedBy field to Session model for admin plugin
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:25:39 +05:00
admins 0e4f6c4b01 Fix impersonation: use direct fetch to /api/auth/admin/impersonate-user
authClient.admin.impersonateUser is not registered in pathMethods
in better-auth v1.6 client plugin — call the endpoint directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:18:49 +05:00
admins dd198349fb Fix impersonation: hard navigation + stop impersonating banner
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:12:47 +05:00
admins 808bcadfca Fix nested list spacing in TipTap lesson content
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:05:33 +05:00
admins ab37af59f2 Fix server component passing event handlers to client components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:59:00 +05:00
admins ce305eab58 Add admin impersonation button to users table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:52:13 +05:00
admins e590f541b3 Add automated backup scripts for PostgreSQL and S3 files to Backblaze B2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 14:38:48 +05:00
admins 48a9398905 Add optional audio response for students in homework submissions
- Course: add allowAudio toggle (per-course setting, off by default)
- HomeworkSubmission: add audioUrl field
- Student: AudioRecorder in homework form when allowAudio is enabled
- Student: show audio player in submission view and curator feedback view
- Curator: show student audio on submission detail page
- AudioRecorder: accept uploadUrl prop (reused for student/curator)
- API: /api/student/audio-upload route for S3 upload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 14:17:46 +05:00
admins 3855bbd4be Add homework review workflow: statuses, audio, file attachments, tabs
- HomeworkSubmission: add status (PENDING/REVIEWING/APPROVED/REJECTED) + statusAt
- HomeworkFeedback: add files (Json) + audioUrl fields
- Curator detail page: meta table, content tabs, feedback history with audio/files
- FeedbackForm: file upload, audio recorder (Web Audio API + S3), action buttons
- AudioRecorder component: record → preview → upload to S3
- ContentTabs: toggle between homework description and lesson content (TipTap read-only)
- Homework list: 4-color status badges with proper filtering
- API routes: /api/curator/upload and /api/curator/audio-upload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 14:01:55 +05:00
admins 768a38b9d3 Add course tree, lesson actions, and module description schema
- CourseTree: expandable module/lesson overview with Eye/Video icons
- SortableLessons: Kinescope ID in create form, published toggle, move-to-module dropdown
- Actions: toggleLessonPublished, moveLessonToModule, updateModule with description
- Schema: add description field to Module model + migration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 13:32:30 +05:00
admins f0024c4243 Add course management improvements: tree view, module descriptions, lesson toggles
- SortableModules: add description textarea in edit form, show description in row
- CourseDetailPage: fetch lessons per module, add CourseTree overview section
- ModulePage: fetch sibling modules, pass as otherModules to SortableLessons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 13:31:30 +05:00
admins d0ba4bf909 Polish: homework filters, users search/popup, admin comments
Homework (/curator/homework):
- Search by student name/email
- Filter by status (pending/reviewed) and course
- Server-side pagination (20 per page) with URL params

Users (/admin/users):
- Search by name/email, filter by role
- Hover popup on each row: enrolled courses + expiry dates + email
- Pagination (20 per page) with URL params

Comments (/admin/comments):
- New admin page with all active comments
- Search by author or text content
- One-click delete (soft-delete) from the table
- Pagination (30 per page)
- Added "Комментарии" link to admin nav

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 13:00:57 +05:00
admins dd46a10c20 Add CSV import/export for students (Stage 11)
Import wizard (4 steps):
- Upload CSV with UTF-8 or Windows-1251 (iconv-lite) decoding
- Auto-detect columns: Email, Имя, Фамилия, Телефон
- Preview table with per-row status: new / update / error
- Options: auto-verify email, assign course + access days, send welcome email
- Apply: creates users with bcrypt password + Account record, grants enrollments

Export:
- GET /api/admin/export-users with course filter + encoding selection
- UTF-8 with BOM (works in all apps) or Windows-1251 (legacy Excel)
- Fields: Email, Имя, Телефон, Дата регистрации, Курсы, Прогресс

Navigation: added "Импорт / Экспорт" link to admin sidebar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 12:51:43 +05:00
admins 99c143d670 Add manual user creation in admin panel
- Server action createUser() with bcrypt password hash + Account record
- Form with name, email, password (show/hide + generate), role, emailVerified toggle
- Optional welcome email toggle (bypasses auto-hook for admin-created users)
- /admin/users/new page with breadcrumb navigation
- After creation, redirects to the new user's profile page
- "Добавить пользователя" button on the users list page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 12:36:52 +05:00
admins 58a61d6f04 Fix settings: catch DB errors at build time, return defaults
getSettings() and getSetting() now fall back to defaults when the
database is unavailable (e.g., during Docker image build).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 12:23:26 +05:00
admins e77588deb8 Add platform settings (Stage 9)
- Settings key-value table in Prisma with migration
- getSettings() / getSetting() helpers in lib/settings.ts
- Admin UI at /admin/settings with 6 sections: General, Notifications,
  Student profile, Legal docs, Curator permissions, Code injection
- saveSettings() server action with admin-only guard
- Maintenance mode: non-admin users redirected to /maintenance page
- schoolName propagated to page metadata and all email templates
- headCode / bodyCode injected into root layout <head> and <body>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 11:18:37 +05:00
admins 093e403f5f Enhance lesson editor: prev/next nav + richer toolbar
- page.tsx: fetch sibling lessons, pass prevLesson/nextLesson props
- LessonEditor: ChevronLeft/Right nav buttons with lesson title tooltip
- Toolbar: added Underline, Strikethrough, inline Code, H1, Horizontal rule,
  Link dialog (prompt), labeled buttons for better discoverability
- Install @tiptap/extension-underline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 10:29:39 +05:00
admins 66b311f17e Polish email template: white outer bg, beige card, Arial font
- Outer background: #FFFFFF
- Card background: #F5F5F0 (beige)
- All text/buttons: Arial/Helvetica (renders consistently on mobile)
- Shadow via table wrapper technique (works in Gmail Android)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 18:05:47 +05:00
admins 32b0fa9d6f Rewrite email template with inline styles for mobile compatibility
- All styles moved to inline attributes (no <style> block)
- Table-based layout for Gmail/Outlook/mobile client compatibility
- Aubade card effect via border-right/border-bottom:4px
- Monospace font stack with web-safe fallbacks
- btn() and quote() helper functions rewritten as <table> elements

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 17:35:30 +05:00
admins c647b29712 Add Markdown import from Obsidian (Stage 8)
- md-to-tiptap.ts: remark-based converter (headings, lists, blockquotes,
  code blocks, bold/italic/strike, links, images, hr)
- Obsidian ![[wikilink]] stripped, [[link|alias]] → plain text
- POST /api/admin/import-md: parses frontmatter (gray-matter) + converts content
- LessonEditor: "Импорт .md" button populates editor without auto-save
- ROADMAP: marked Stages 2, 3, 5, 6, 7, 8 as complete, fixed numbering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 15:44:42 +05:00
admins 6d93a7b406 Add lesson comments (Stage 6)
- LessonComment CRUD: addComment / deleteComment server actions
- LessonComments client component with form, avatar, delete
- Comments section at bottom of lesson page (enrolled users only)
- Soft-delete support, moderation for curator/admin
- ROADMAP: moved Квизы to end (Stage 11), marked Stage 7 done

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 15:33:47 +05:00
admins 97f4c1ec24 Fix admin sidebar missing on /curator/* routes
- Extract AdminShell component (sidebar + wrapper)
- admin/layout.tsx uses AdminShell
- curator/layout.tsx uses AdminShell for admin role (was rendering children only)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 14:59:51 +05:00
admins ec51dd34bb Replace admin dashboard stub with real stats
- 4 stat cards: students (+monthly), courses (published), active enrollments (expiring alert), homework pending
- Recent enrollments list (last 8)
- Top courses by enrollment count
- Activity counters: total lessons completed, total homework submitted
- All cards link to relevant admin pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 14:58:46 +05:00
admins b40d518b74 Fix Resend lazy init to avoid build-time API key error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 14:48:14 +05:00
admins 6975a9f97e Add email notifications via Resend
- src/lib/email.ts: HTML templates for 4 email types (Second Brain design)
- Welcome email on user registration (Better Auth databaseHooks)
- Course access email when admin grants enrollment
- Homework submitted email to all admins/curators (first submission only)
- Feedback received email to student with feedback text and lesson link
- Update TECHNICAL.md: Resend domain, from-address, email vars, Stage 3 summary

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 14:46:46 +05:00
admins 9bc18247df Add homework review link to admin sidebar
- Admin sidebar now has "ДЗ на проверку" link → /curator/homework
- Curator layout renders children-only for admin (no double sidebar)
- Active state highlights correctly when on /curator/* routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 14:18:56 +05:00
admins 543d5b2d5e Add homework system (admin, student, curator)
Admin:
- HomeworkEditor in lesson page: create/update/delete assignment description

Student:
- HomeworkSection in lesson page: view assignment, submit text + files
- Resubmission allowed until curator gives feedback
- Shows feedback from curator with date and name

Curator:
- New layout with Second Brain dark sidebar (replaces green theme)
- /curator/dashboard: stats cards (pending, total, reviewed this week)
- /curator/homework: list of all submissions, pending highlighted
- /curator/homework/[id]: review submission, write feedback, redirect after send

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 14:13:24 +05:00
admins d0c8c6dd53 Add lesson progress tracking
- Toggle lesson completion via server action (LessonProgress table)
- "Отметить как пройденный" button on lesson page, turns accent when done
- Course sidebar: progress bar, checkmarks on completed lessons, X/Y counter per module
- Dashboard: progress bar on each course card with completion percentage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 13:16:28 +05:00
admins c88b5d2004 Allow admin to preview unpublished lessons without enrollment
- Course layout skips enrollment and published checks for admin role
- Lesson page skips published filter for admin role
- Enables admin preview button to work for any lesson/course state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 12:57:18 +05:00
admins 4183a912e4 Add save and preview icons to lesson editor
- Save button now shows floppy disk icon (lucide Save)
- New Preview button with eye icon opens lesson in student view (new tab)
- Pass courseSlug through to LessonEditor for preview URL construction

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 12:45:30 +05:00
admins 07b9a6d261 Polish UX: auto-redirect on create, fix design consistency
- createModule now redirects to module page after creation
- createLesson now redirects to lesson editor after creation
- Regenerate Prisma client to fix missing types (category, accessLog, expiresAt)
- Rewrite sortable-modules/lessons with Second Brain design tokens (remove amber/slate)
- Rewrite lesson-editor toolbar and toggle with design tokens
- Fix register page/form: replace amber theme with card-aubade design

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 12:38:46 +05:00
admins 05dd4d1df2 Stage 2: student lesson viewer, Kinescope player, PDF files, prev/next nav, My Courses dashboard 2026-04-07 12:13:12 +05:00
admins 03e3972388 Add TECHNICAL.md: infrastructure, design tokens, media specs, DB schema, done stages 2026-04-07 12:08:44 +05:00
admins 8fdc67b4a5 Mark Stage 1.5 complete in ROADMAP 2026-04-07 12:01:19 +05:00
admins e9eff5bae5 Stage 1.5: categories, enrollment expiry, access log, bulk grant, user page 2026-04-07 11:59:13 +05:00
admins 992763aeb9 Apply Second Brain design: Fira Mono, Aubade cards, brand palette 2026-04-07 11:51:20 +05:00
admins 09325187f9 Fix createLesson return type: void instead of string 2026-04-07 11:39:51 +05:00
admins 01a9ef482c Fix DialogTrigger: remove asChild (base-ui doesn't support it) 2026-04-07 11:38:33 +05:00
admins d356dddc96 Stage 1: Course/Module/Lesson CRUD admin UI with TipTap editor 2026-04-07 11:36:27 +05:00
admins 9d82b73e58 feat: initial commit 2026-04-07 11:30:38 +05:00
425 changed files with 45023 additions and 365 deletions
-1
View File
@@ -5,7 +5,6 @@
.env.production .env.production
node_modules node_modules
.next .next
src/generated
*.md *.md
docker-compose.yml docker-compose.yml
docker-compose.prod.yml docker-compose.prod.yml
+12 -2
View File
@@ -12,5 +12,15 @@ S3_ACCESS_KEY=""
S3_SECRET_KEY="" S3_SECRET_KEY=""
S3_REGION="eu-central" S3_REGION="eu-central"
# Kinescope (добавить при получении платного плана) # Cloudflare Turnstile (anti-bot защита на регистрации)
# KINESCOPE_API_KEY="" NEXT_PUBLIC_TURNSTILE_SITE_KEY=""
TURNSTILE_SECRET_KEY=""
# Kinescope API
KINESCOPE_API_KEY=""
# Internal access-provisioning (payment-router → /api/internal/grant)
INTERNAL_GRANT_SECRET=""
# Freemium: slug бесплатного курса, в который авто-записываем новых юзеров при регистрации
FREE_COURSE_SLUG=obsidian-start
+3
View File
@@ -45,3 +45,6 @@ yarn-error.log*
next-env.d.ts next-env.d.ts
/src/generated/prisma /src/generated/prisma
# Claude Code local plugins (external git repos, не коммитим)
.claude/plugins/
+11
View File
@@ -0,0 +1,11 @@
# Auto-generated Prisma client — раздувает граф 28 псевдо-сообществами
src/generated/
# Superpowers / Claude Code инфраструктура, к LMS-коду не относится
.claude/
# Build/cache артефакты (на всякий случай, помимо .gitignore)
.next/
out/
build/
coverage/
View File
@@ -0,0 +1,86 @@
<h2>Как выглядит раздел «Вопросы» в админке?</h2>
<p class="subtitle">Список + открытый тред — выбери что ближе по ощущению</p>
<div class="options">
<div class="option" data-choice="a" onclick="toggleSelect(this)">
<div class="letter">A</div>
<div class="content">
<h3>Список + боковая панель треда</h3>
<p>Слева список вопросов, справа открывается тред. Как почтовый клиент — не уходишь со страницы.</p>
<div class="mockup" style="margin-top:12px">
<div class="mockup-header">Admin → Вопросы учеников</div>
<div class="mockup-body" style="padding:0; display:flex; min-height:160px; font-size:12px">
<div style="width:45%; border-right:2px solid #AAAAAA; padding:8px">
<div style="margin-bottom:6px; display:flex; gap:4px">
<span style="padding:2px 8px; background:#323232; color:#F5F5F0; border-radius:2px; font-size:10px">Открытые 4</span>
<span style="padding:2px 8px; background:#E8E8E0; border-radius:2px; font-size:10px">Закрытые</span>
</div>
<div style="padding:6px; background:#E8F0D8; border-left:3px solid #323232; margin-bottom:4px">
<div style="font-weight:700">Алексей М.</div>
<div style="color:#666; font-size:11px">Как экспортировать в PDF?</div>
<div style="color:#999; font-size:10px">15 мин назад · 🔴 новое</div>
</div>
<div style="padding:6px; background:#F5F5F0; border-left:3px solid transparent; margin-bottom:4px; border:1px solid #E8E8E0">
<div>Мария К.</div>
<div style="color:#666; font-size:11px">Плагины не устанавливаются</div>
<div style="color:#999; font-size:10px">2 часа назад</div>
</div>
<div style="padding:6px; background:#F5F5F0; border:1px solid #E8E8E0">
<div>Иван С.</div>
<div style="color:#666; font-size:11px">Синхронизация с телефоном</div>
<div style="color:#999; font-size:10px">вчера · 🔴 новое</div>
</div>
</div>
<div style="flex:1; padding:8px; display:flex; flex-direction:column">
<div style="font-weight:700; margin-bottom:2px; font-size:13px">Как экспортировать в PDF?</div>
<div style="color:#666; font-size:11px; margin-bottom:8px">Алексей М. · Obsidian. Полный курс</div>
<div style="background:#E8E8E0; padding:6px; border-radius:2px; margin-bottom:6px; font-size:12px">
Привет! Пытаюсь экспортировать заметки в PDF через Pandoc, но получаю ошибку...
</div>
<div style="margin-top:auto; display:flex; gap:4px">
<input style="flex:1; padding:4px 6px; border:1px solid #AAAAAA; font-size:11px; border-radius:2px" placeholder="Ответить...">
<button style="padding:4px 8px; background:#323232; color:#F5F5F0; border:none; font-size:11px; border-radius:2px"></button>
<button style="padding:4px 8px; background:#E8E8E0; border:1px solid #AAAAAA; font-size:10px; border-radius:2px">✓ Закрыть</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="option" data-choice="b" onclick="toggleSelect(this)">
<div class="letter">B</div>
<div class="content">
<h3>Список → отдельная страница треда</h3>
<p>Список всех вопросов, клик открывает отдельную страницу. Проще в реализации, больше места для переписки.</p>
<div class="mockup" style="margin-top:12px">
<div class="mockup-header">Admin → Вопросы / #42 — Алексей М.</div>
<div class="mockup-body" style="padding:8px; font-size:12px">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px">
<span style="font-weight:700">Как экспортировать в PDF?</span>
<span style="padding:2px 8px; background:#E8F0D8; border:1px solid #AAAAAA; font-size:10px; border-radius:2px">● ОТКРЫТ</span>
</div>
<div style="display:flex; flex-direction:column; gap:5px; margin-bottom:8px">
<div style="background:#E8E8E0; padding:5px 8px; border-radius:2px; max-width:80%">
<div style="font-size:10px; color:#666">Алексей · 14:32</div>
Пытаюсь экспортировать через Pandoc, получаю ошибку...
</div>
<div style="background:#F5F5F0; border:2px solid #E8F0D8; padding:5px 8px; border-radius:2px; max-width:80%; align-self:flex-end">
<div style="font-size:10px; color:#666">Дмитрий (admin) · 14:55</div>
Покажи текст ошибки?
</div>
<div style="background:#E8E8E0; padding:5px 8px; border-radius:2px; max-width:80%">
<div style="font-size:10px; color:#666">Алексей · 15:01</div>
"Error: pandoc not found in PATH"
</div>
</div>
<div style="display:flex; gap:4px">
<input style="flex:1; padding:4px 6px; border:1px solid #AAAAAA; font-size:11px; border-radius:2px" placeholder="Написать ответ...">
<button style="padding:4px 10px; background:#323232; color:#F5F5F0; border:none; font-size:11px; border-radius:2px">Отправить</button>
<button style="padding:4px 8px; background:#E8E8E0; border:1px solid #AAAAAA; font-size:10px; border-radius:2px">✓ Закрыть вопрос</button>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,56 @@
<h2>Как устроен диалог после отправки вопроса?</h2>
<p class="subtitle">Влияет на структуру БД и сложность реализации</p>
<div class="options">
<div class="option" data-choice="a" onclick="toggleSelect(this)">
<div class="letter">A</div>
<div class="content">
<h3>Один вопрос → один ответ</h3>
<p>Студент задаёт вопрос. Админ отвечает один раз. Закрыто. Просто и быстро в реализации.</p>
<div class="mockup" style="margin-top:12px">
<div class="mockup-header">Страница вопроса у студента</div>
<div class="mockup-body" style="padding:12px; font-size:13px">
<div style="background:#E8E8E0; padding:8px; border-radius:2px; margin-bottom:8px">
<div style="font-size:11px; color:#666; margin-bottom:4px">Ты · 14 мая</div>
<div>Как правильно настроить шаблоны в Obsidian?</div>
</div>
<div style="background:#E8F0D8; border:2px solid #323232; padding:8px; border-radius:2px">
<div style="font-size:11px; color:#666; margin-bottom:4px">Дмитрий · 15 мая ✅ Отвечено</div>
<div>Зайди в Настройки → Шаблоны → укажи папку...</div>
</div>
</div>
</div>
</div>
</div>
<div class="option" data-choice="b" onclick="toggleSelect(this)">
<div class="letter">B</div>
<div class="content">
<h3>Тред (переписка)</h3>
<p>Студент и админ могут писать несколько сообщений. Как поддержка-чат внутри платформы.</p>
<div class="mockup" style="margin-top:12px">
<div class="mockup-header">Тред вопроса</div>
<div class="mockup-body" style="padding:12px; font-size:13px">
<div style="background:#E8E8E0; padding:6px 8px; border-radius:2px; margin-bottom:6px; margin-left:0; max-width:85%">
<div style="font-size:10px; color:#666">Ты · 14 мая</div>
Как настроить шаблоны?
</div>
<div style="background:#E8F0D8; padding:6px 8px; border-radius:2px; margin-bottom:6px; margin-left:auto; max-width:85%; text-align:right">
<div style="font-size:10px; color:#666">Дмитрий · 15 мая</div>
Зайди в Настройки → Шаблоны...
</div>
<div style="background:#E8E8E0; padding:6px 8px; border-radius:2px; margin-bottom:6px; max-width:85%">
<div style="font-size:10px; color:#666">Ты · 15 мая</div>
А какую папку лучше назвать?
</div>
<div style="border:1px solid #AAAAAA; border-radius:2px; padding:6px 8px; color:#999; font-size:12px">Написать сообщение...</div>
</div>
</div>
</div>
</div>
</div>
<div class="section" style="margin-top:16px; padding:12px; background:#F5F5F0; border:1px solid #AAAAAA; border-radius:2px">
<span class="label">Рекомендация</span>
<p style="margin:4px 0 0; font-size:14px">Вариант А проще, закрывает 90% случаев. Тред (Б) полезен когда ответ требует уточнений, но добавляет ~40% сложности реализации.</p>
</div>
@@ -0,0 +1,56 @@
<h2>Где студент начинает «Задать вопрос»?</h2>
<p class="subtitle">Влияет на то, будет ли вопрос привязан к уроку или нет</p>
<div class="options">
<div class="option" data-choice="a" onclick="toggleSelect(this)">
<div class="letter">A</div>
<div class="content">
<h3>Только главное меню</h3>
<p>Пункт «Задать вопрос» в хедере / sidebar студента. Общий вопрос без привязки к уроку.</p>
<div class="mockup" style="margin-top:12px">
<div class="mockup-header">Студент — sidebar</div>
<div class="mockup-body" style="padding:12px; font-size:13px">
<div style="padding:8px; margin-bottom:4px; background:#E8E8E0; border-radius:2px">📚 Мои курсы</div>
<div style="padding:8px; margin-bottom:4px; background:#E8E8E0; border-radius:2px">👤 Профиль</div>
<div style="padding:8px; background:#E8F0D8; border:2px solid #323232; border-radius:2px; font-weight:700">❓ Задать вопрос</div>
</div>
</div>
</div>
</div>
<div class="option" data-choice="b" onclick="toggleSelect(this)">
<div class="letter">B</div>
<div class="content">
<h3>Только со страницы урока</h3>
<p>Кнопка на странице урока рядом с комментариями. Вопрос автоматически привязан к курсу и уроку.</p>
<div class="mockup" style="margin-top:12px">
<div class="mockup-header">Страница урока — низ</div>
<div class="mockup-body" style="padding:12px; font-size:13px">
<div style="margin-bottom:8px; color:#666">💬 Комментарии (3)</div>
<div style="display:flex; gap:8px">
<div style="padding:6px 12px; background:#E8E8E0; border:1px solid #AAAAAA; border-radius:2px; font-size:12px">Написать комментарий</div>
<div style="padding:6px 12px; background:#F5F5F0; border:2px solid #323232; border-radius:2px; font-size:12px; font-weight:700">❓ Задать приватный вопрос</div>
</div>
</div>
</div>
</div>
</div>
<div class="option" data-choice="c" onclick="toggleSelect(this)">
<div class="letter">C</div>
<div class="content">
<h3>Оба места</h3>
<p>Пункт в меню для общих вопросов + кнопка на уроке для вопросов по конкретному уроку. Контекст подставляется автоматически.</p>
<div class="mockup" style="margin-top:12px">
<div class="mockup-header">Два источника → одна очередь у админа</div>
<div class="mockup-body" style="padding:12px; font-size:13px">
<div style="display:flex; gap:8px; margin-bottom:8px">
<div style="padding:6px 10px; background:#E8F0D8; border:2px solid #323232; font-size:11px; font-weight:700; border-radius:2px">Меню → общий вопрос</div>
<div style="padding:6px 10px; background:#E8F0D8; border:2px solid #323232; font-size:11px; font-weight:700; border-radius:2px">Урок → вопрос по уроку</div>
</div>
<div style="padding:8px; border:1px solid #AAAAAA; border-radius:2px; color:#666; font-size:12px">→ Один список у админа: «Вопросы учеников»</div>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,108 @@
<h2>Студент — раздел «Мои вопросы»</h2>
<p class="subtitle">Список + открытый тред. Скажи что поправить.</p>
<div class="split" style="gap:16px; align-items:flex-start">
<!-- Список вопросов -->
<div class="mockup" style="flex:1">
<div class="mockup-header">/questions — Мои вопросы</div>
<div class="mockup-body" style="padding:12px; font-size:13px">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px">
<span style="font-weight:700; font-size:15px">Мои вопросы</span>
<button style="padding:6px 14px; background:#F5F5F0; border:2px solid #323232; font-size:12px; font-weight:700; border-radius:2px; cursor:pointer">+ Задать вопрос</button>
</div>
<!-- Открытый вопрос с новым ответом -->
<div style="border:2px solid #323232; border-radius:2px; padding:10px; margin-bottom:8px; background:#F5F5F0; cursor:pointer">
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:4px">
<span style="font-weight:700; font-size:13px">Как настроить синхронизацию с телефоном?</span>
<span style="padding:2px 7px; background:#E8F0D8; border:1px solid #AAAAAA; font-size:10px; border-radius:2px; white-space:nowrap; margin-left:8px">● ОТКРЫТ</span>
</div>
<div style="color:#666; font-size:11px; margin-bottom:6px">3 сообщения · последнее от Дмитрия</div>
<div style="display:flex; align-items:center; gap:6px">
<span style="display:inline-block; width:8px; height:8px; background:#323232; border-radius:50%"></span>
<span style="font-size:11px; font-weight:700">Новый ответ от школы</span>
<span style="color:#999; font-size:11px">· 20 мин назад</span>
</div>
</div>
<!-- Обычный открытый вопрос -->
<div style="border:1px solid #AAAAAA; border-radius:2px; padding:10px; margin-bottom:8px; background:#F5F5F0; cursor:pointer">
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:4px">
<span style="font-size:13px">Ошибка при установке плагина Dataview</span>
<span style="padding:2px 7px; background:#E8F0D8; border:1px solid #AAAAAA; font-size:10px; border-radius:2px; white-space:nowrap; margin-left:8px">● ОТКРЫТ</span>
</div>
<div style="color:#666; font-size:11px">2 сообщения · ожидает ответа школы</div>
</div>
<!-- Закрытый вопрос -->
<div style="border:1px solid #E8E8E0; border-radius:2px; padding:10px; background:#FAFAFA; cursor:pointer; opacity:0.7">
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:4px">
<span style="font-size:13px; color:#666">Как экспортировать заметки в PDF?</span>
<span style="padding:2px 7px; background:#E8E8E0; font-size:10px; border-radius:2px; white-space:nowrap; margin-left:8px; color:#666">✓ ЗАКРЫТ</span>
</div>
<div style="color:#999; font-size:11px">5 сообщений · закрыт 10 мая</div>
</div>
</div>
</div>
<!-- Страница треда -->
<div class="mockup" style="flex:1.2">
<div class="mockup-header">/questions/[id] — Открытый тред</div>
<div class="mockup-body" style="padding:12px; font-size:13px">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px">
<div>
<div style="font-weight:700; font-size:14px">Как настроить синхронизацию с телефоном?</div>
<div style="font-size:11px; color:#666">Создан 18 мая · <span style="color:#323232; font-weight:700">● Открыт</span></div>
</div>
<a href="#" style="font-size:11px; color:#666; text-decoration:underline">← Все вопросы</a>
</div>
<!-- Сообщения треда -->
<div style="display:flex; flex-direction:column; gap:8px; margin-bottom:12px; max-height:200px; overflow-y:auto">
<div style="background:#E8E8E0; padding:8px 10px; border-radius:2px; max-width:88%">
<div style="font-size:10px; color:#666; margin-bottom:3px">Ты · 18 мая, 10:14</div>
<div>Привет! Хочу синхронизировать хранилище между Mac и iPhone. Пробовал iCloud, но заметки иногда конфликтуют. Есть ли рекомендованный способ?</div>
</div>
<div style="background:#F5F5F0; border:2px solid #E8F0D8; padding:8px 10px; border-radius:2px; max-width:88%; align-self:flex-end">
<div style="font-size:10px; color:#666; margin-bottom:3px">Дмитрий (куратор) · 18 мая, 11:30</div>
<div>Рекомендую Obsidian Sync — он создан специально для этого. Или как альтернатива — Working Copy + Git. Какой вариант тебе ближе?</div>
</div>
<div style="background:#E8E8E0; padding:8px 10px; border-radius:2px; max-width:88%">
<div style="font-size:10px; color:#666; margin-bottom:3px">Ты · 18 мая, 11:45</div>
<div>Лучше без платной подписки если можно. Расскажи про Working Copy?</div>
<!-- файл -->
<div style="margin-top:6px; display:flex; align-items:center; gap:5px; padding:4px 7px; background:#F5F5F0; border:1px solid #AAAAAA; border-radius:2px; font-size:11px; width:fit-content">
<span>📎</span> <span>screenshot.png</span> <span style="color:#999">· 84 KB</span>
</div>
</div>
<div style="background:#F5F5F0; border:2px solid #E8F0D8; padding:8px 10px; border-radius:2px; max-width:88%; align-self:flex-end; border-left:3px solid #323232">
<div style="font-size:10px; color:#666; margin-bottom:3px">Дмитрий (куратор) · 18 мая, 20 мин назад · <strong>🔵 новое</strong></div>
<div>Working Copy — это Git-клиент для iOS. Схема: хранилище в приватном репо GitHub, Working Copy на телефоне синкает его автоматически...</div>
</div>
</div>
<!-- Форма ответа -->
<div style="border:2px solid #AAAAAA; border-radius:2px; padding:8px; background:#F5F5F0">
<textarea style="width:100%; border:none; background:transparent; font-size:13px; resize:none; outline:none; font-family:inherit; min-height:50px" placeholder="Написать сообщение..."></textarea>
<div style="display:flex; justify-content:space-between; align-items:center; margin-top:6px; padding-top:6px; border-top:1px solid #E8E8E0">
<div style="display:flex; align-items:center; gap:6px">
<button style="padding:4px 8px; background:#F5F5F0; border:1px solid #AAAAAA; font-size:11px; border-radius:2px; cursor:pointer">📎 Прикрепить</button>
<span style="font-size:10px; color:#999">jpg, png, pdf, md · до 10 МБ</span>
</div>
<button style="padding:6px 16px; background:#323232; color:#F5F5F0; border:none; font-size:12px; font-weight:700; border-radius:2px; cursor:pointer">Отправить →</button>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,3 @@
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
<p class="subtitle">Continuing in terminal...</p>
</div>
@@ -0,0 +1,3 @@
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
<p class="subtitle">Presenting design in terminal...</p>
</div>
@@ -0,0 +1 @@
{"reason":"idle timeout","timestamp":1779178878212}
@@ -0,0 +1,10 @@
{"type":"server-started","port":49771,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:49771","screen_dir":"/Users/dementiy/Documents/Claude/lms-system/.superpowers/brainstorm/8559-1779175817/content","state_dir":"/Users/dementiy/Documents/Claude/lms-system/.superpowers/brainstorm/8559-1779175817/state"}
{"type":"screen-added","file":"/Users/dementiy/Documents/Claude/lms-system/.superpowers/brainstorm/8559-1779175817/content/entry-point.html"}
{"source":"user-event","type":"click","text":"A\n \n Только главное меню\n Пункт «Задать вопрос» в хедере / sidebar студента. Общий вопрос без привязки к уроку.\n \n Студент — sidebar\n \n 📚 Мои курсы\n 👤 Профиль\n ❓ Задать вопрос","choice":"a","id":null,"timestamp":1779175897914}
{"type":"screen-added","file":"/Users/dementiy/Documents/Claude/lms-system/.superpowers/brainstorm/8559-1779175817/content/conversation-model.html"}
{"source":"user-event","type":"click","text":"B\n \n Тред (переписка)\n Студент и админ могут писать несколько сообщений. Как поддержка-чат внутри платформы.\n \n Тред вопроса\n \n \n Ты · 14 мая\n Как настроить шаблоны?\n \n \n Дмитрий · 15 мая\n Зайди в Настройки → Шаблоны...\n \n \n Ты · 15 мая\n А какую папку лучше назвать?\n \n Написать сообщение...","choice":"b","id":null,"timestamp":1779176002499}
{"type":"screen-added","file":"/Users/dementiy/Documents/Claude/lms-system/.superpowers/brainstorm/8559-1779175817/content/waiting-1.html"}
{"type":"screen-added","file":"/Users/dementiy/Documents/Claude/lms-system/.superpowers/brainstorm/8559-1779175817/content/admin-views.html"}
{"type":"screen-added","file":"/Users/dementiy/Documents/Claude/lms-system/.superpowers/brainstorm/8559-1779175817/content/waiting-2.html"}
{"type":"screen-added","file":"/Users/dementiy/Documents/Claude/lms-system/.superpowers/brainstorm/8559-1779175817/content/student-questions.html"}
{"type":"server-stopped","reason":"idle timeout"}
@@ -0,0 +1 @@
8568
+307 -4
View File
@@ -1,5 +1,308 @@
<!-- BEGIN:nextjs-agent-rules --> # AGENTS.md — LMS Second Brain
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. Собственная LMS-платформа для образовательных курсов по PKM и Obsidian.
<!-- END:nextjs-agent-rules --> Заменяет 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
- Используется `proxy.ts` вместо `middleware.ts`
- Экспортируемая функция называется `proxy`, не `middleware`
- Перед написанием кода смотри `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-типы
│ ├── proxy.ts # Auth middleware (защита маршрутов)
│ └── middleware.ts # Обёртка над proxy
├── 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/proxy.ts` + проверка сессии в 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
+1 -1
View File
@@ -11,7 +11,7 @@
| TypeScript | 5.x | Язык | | TypeScript | 5.x | Язык |
| React | 19 | UI | | React | 19 | UI |
| PostgreSQL | 16 | База данных | | PostgreSQL | 16 | База данных |
| Prisma | 6.x | ORM + миграции | | Prisma | 7.x | ORM + миграции |
| Better Auth | latest | Аутентификация и сессии | | Better Auth | latest | Аутентификация и сессии |
| Tailwind CSS | 4.x (CSS-based config) | Стили (нет tailwind.config.ts) | | Tailwind CSS | 4.x (CSS-based config) | Стили (нет tailwind.config.ts) |
| shadcn/ui | latest | UI-компоненты | | shadcn/ui | latest | UI-компоненты |
+34
View File
@@ -0,0 +1,34 @@
# Дизайн-система — LMS
Этот проект использует дизайн-систему **ДС-2 «Second Brain LMS & Press»** — терминальный язык «Aubade».
## Канон
Полная спецификация (9 секций, формат `DESIGN.md`):
- **Source of truth:** `SecondBrainTech/02-Стандарты/Дизайн-LMS/DESIGN.md`
- Копия для Open Design: `~/Documents/Claude/open-design/design-systems/second-brain-lms/`
- Превью со всеми примерами: `preview.html` в тех же каталогах
> Канон правится только там. Этот файл — практический указатель для разработки внутри репозитория.
## Язык в двух словах
Терминальный, моноширинный, «реестровый». Серо-зелёная палитра, острые углы 2px, выраженные рамки 2px, жёсткие тени-подложки с физикой hover/active. Тёмный админ-сайдбар. Без кремовых тонов и серифа — это язык ДС-1 (сайт и PDF), отдельной парной системы.
## Где токены в этом репозитории
Реализация — `src/app/globals.css`:
- **Палитра** — CSS-переменные в `:root`: `--background #F5F5F0`, `--foreground #323232`, `--accent #E8F0D8`, `--border #AAAAAA`, тёмный сайдбар `--sidebar-*`.
- **Типографическая шкала** — переопределённые токены Tailwind `--text-*` в блоке `@theme` (канон ДС-2, +2px к дефолту Tailwind).
- **Шрифт** — Fira Mono, подключение в `src/app/layout.tsx` через `next/font/google`.
- **Компонентные классы** — `.card-aubade`, `.btn-aubade`, `.btn-aubade-accent`, `.tag-aubade`, `.admin-sidebar*`.
## Письма Press
Рассылка Second Brain Press — поверхность Email той же ДС-2. Шаблон — Listmonk template id=1 (табличная вёрстка, Arial, карта 620px с рамкой 2px `#AAAAAA`). Подробности — секция 5 канонического `DESIGN.md`.
## История
Предыдущая версия этого файла ссылалась на легаси-дизайн-систему v1 (кремовая палитра + лаванда, `~/Documents/Claude/design-system/`). Она заменена: v1 — легаси, актуальна ДС-2.
+3
View File
@@ -9,6 +9,9 @@ FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
# NEXT_PUBLIC_* vars are inlined at build time
ARG NEXT_PUBLIC_TURNSTILE_SITE_KEY
ENV NEXT_PUBLIC_TURNSTILE_SITE_KEY=$NEXT_PUBLIC_TURNSTILE_SITE_KEY
# Generate Prisma client before build # Generate Prisma client before build
RUN npx prisma generate RUN npx prisma generate
RUN npm run build RUN npm run build
+289 -142
View File
@@ -6,13 +6,12 @@
--- ---
## Этап 0 — Каркас, auth, роли ✅ ЗАВЕРШЁН (07.04.2026) ## Этап 0 — Каркас, auth, роли ✅ ЗАВЕРШЁН (07.04.2026)
**Задеплоено на:** https://school.second-brain.ru
- [x] Next.js 16.2.2 (App Router, TypeScript, Tailwind v4) - [x] Next.js 16.2.2 (App Router, TypeScript, Tailwind v4)
- [x] Docker Compose: PostgreSQL 16 (прод на Hetzner) - [x] Docker Compose: PostgreSQL 16 (прод на Hetzner)
- [x] Prisma 7: схема User, Session, Account + полная LMS-модель - [x] Prisma 7: схема User, Session, Account + полная LMS-модель
- [x] Better Auth: вход по email/password, роли student/curator/admin - [x] Better Auth: вход по email/password, роли student/curator/admin
- [x] proxy.ts: защита маршрутов по сессии - [x] Middleware: защита маршрутов по сессии
- [x] Дашборды для трёх ролей - [x] Дашборды для трёх ролей
- [x] Страница входа, регистрации, подтверждения email - [x] Страница входа, регистрации, подтверждения email
- [x] Seed: admin/curator/student (пароль: Password123!) - [x] Seed: admin/curator/student (пароль: Password123!)
@@ -21,62 +20,249 @@
--- ---
## Этап 1 — Курсы → Модули → Уроки (CRUD в админке) ## Этап 1 — Курсы → Модули → Уроки (CRUD в админке) ✅ ЗАВЕРШЁН (07.04.2026)
**Цель:** могу создать полную структуру курса из браузера.
- [ ] Prisma-схема: Course, Module, Lesson (с порядком, статусом published) - [x] Prisma-схема: Course, Module, Lesson (с порядком, статусом published)
- [ ] Admin: список курсов, создать / редактировать / удалить курс - [x] Admin: список курсов, создать / редактировать / удалить курс
- [ ] Admin: список модулей внутри курса, drag-and-drop сортировка - [x] Admin: список модулей внутри курса, drag-and-drop сортировка
- [ ] Admin: список уроков внутри модуля, drag-and-drop сортировка - [x] Admin: список уроков внутри модуля, drag-and-drop сортировка
- [ ] Admin: редактор урока с TipTap (заголовки, списки, цитаты, код, картинки, ссылки) - [x] Admin: редактор урока с TipTap (заголовки, списки, цитаты, код, картинки, ссылки)
- [ ] Загрузка картинок в уроке → Hetzner Object Storage - [x] Загрузка картинок в уроке → Hetzner Object Storage
- [ ] Поле для Kinescope ID в уроке (просто текстовое, без интеграции — это Этап 2) - [x] Поле для Kinescope ID в уроке
- [ ] Публикация/скрытие курса и урока (черновик / опубликован) - [x] Публикация/скрытие курса и урока (черновик / опубликован)
- [ ] Управление доступом: выдать / забрать доступ к курсу для пользователя - [x] Управление доступом: выдать / забрать доступ к курсу для пользователя
- [x] Дизайн: Fira Mono, #F5F5F0, Aubade-карточки
**Критерий готовности:** создаю курс «Obsidian PKM», добавляю 2 модуля, в каждом по 3 урока с текстом и картинкой, публикую. - [x] Admin: таблица пользователей (/admin/users)
--- ---
## Этап 2 — Интеграция Kinescope, рендер уроков для ученика **Доработки таблицы пользователей (добавить в рамках Этапа 9):**
**Цель:** ученик видит урок с видео Kinescope и текстом. - [ ] Фильтры: по роли (Ученик / Куратор / Администратор), по статусу email (подтверждён/нет)
- [ ] Поиск по имени / email
- [ ] Компонент `KinescopePlayer` — обёртка над `@kinescope/react-kinescope-player` - [ ] Пагинация + выбор кол-ва записей на странице (20/50/100)
- [ ] Рендер урока для ученика: видео (если есть Kinescope ID) + текст + файлы - [ ] Ховер-попап на строке: список курсов пользователя со сроками доступа + email + телефон
- [ ] Загрузка PDF/файлов к уроку (Object Storage), список для скачивания - [ ] Быстрые действия в строке: кнопка «Выдать доступ» без перехода на страницу пользователя
- [ ] Страница курса для ученика: список модулей и уроков, статус прохождения
- [ ] Навигация по урокам: предыдущий / следующий
- [ ] Блокировка доступа к курсу без enrollment (middleware или server component)
- [ ] Страница «Мои курсы» в личном кабинете ученика
**Кинескоп-нюанс:** пока без DRM (бесплатный план), просто передаём `videoId` в компонент. Когда появится платный план — добавим signed URLs в отдельной задаче.
**Критерий готовности:** ученик открывает урок, смотрит видео из Kinescope, читает текст, скачивает PDF.
--- ---
## Этап 3 — Прогресс и линейное открытие уроков ## Этап 1.5 — Расширенное управление доступом ✅ ЗАВЕРШЁН (07.04.2026)
**Цель:** ученик проходит курс последовательно, прогресс сохраняется.
- [ ] Prisma: таблица `LessonProgress` (userId, lessonId, completedAt) - [x] Срок доступа: `expiresAt` в `CourseEnrollment`, просроченный подсвечивается красным
- [ ] Кнопка «Урок завершён» — создаёт запись в LessonProgress - [x] Категории курсов: таблица `Category`, `/admin/categories`, привязка к курсу
- [ ] Логика блокировки: следующий урок открыт только если предыдущий завершён - [x] Расширенный энролл: `/admin/users/[userId]` — выбор нескольких курсов + срок одной операцией
- [ ] Прогресс-бар по курсу (% завершённых уроков) - [x] История доступа: `AccessLog` — каждая операция логируется (кто, когда, метод, примечание)
- [ ] Прогресс-бар по модулю
- [ ] API: `POST /api/progress/complete` — принимает lessonId, создаёт прогресс
- [ ] Иконки статуса уроков в боковой панели: ✓ пройден / 🔒 заблокирован / → текущий
**Примечание:** если в уроке есть тест (Этап 4) или ДЗ (Этап 5) — кнопка «Завершить» появляется только после их прохождения/отправки. Эту логику дорабатываем на соответствующих этапах.
**Критерий готовности:** прохожу урок 1, нажимаю «Завершён» — открывается урок 2. Урок 3 заблокирован. Прогресс-бар показывает 33%.
--- ---
## Этап 4Тесты и квизы ## Этап 2Интеграция Kinescope, рендер уроков для ученика ✅ ЗАВЕРШЁН (07.04.2026)
- [x] Компонент `KinescopePlayer` — обёртка над `@kinescope/react-kinescope-player`
- [x] Рендер урока для ученика: видео (если есть Kinescope ID) + текст + файлы
- [x] Загрузка PDF/файлов к уроку (Object Storage), список для скачивания
- [x] Страница курса для ученика: список модулей и уроков, статус прохождения
- [x] Навигация по урокам: предыдущий / следующий
- [x] Блокировка доступа к курсу без enrollment (layout server component)
- [x] Страница «Мои курсы» в личном кабинете ученика (dashboard)
- [x] Кнопки Сохранить / Просмотр в редакторе урока
- [x] Иконка-статус уроков в боковой панели курса (✓ пройден)
---
## Этап 3 — Прогресс ✅ ЗАВЕРШЁН (07.04.2026)
- [x] Prisma: таблица `LessonProgress` (userId, lessonId, completedAt)
- [x] Кнопка «Урок завершён» — создаёт/удаляет запись в LessonProgress
- [x] Прогресс-бар по курсу в боковой панели (% завершённых уроков)
- [x] Прогресс-бар по курсу на дашборде студента
- [x] Admin bypass: администратор видит все уроки без отметок о прогрессе
---
## Этап 5 — Домашние задания и обратная связь куратора ✅ ЗАВЕРШЁН (07.04.2026)
- [x] Prisma: Homework, HomeworkSubmission, HomeworkFeedback
- [x] Admin: добавить / редактировать / удалить блок ДЗ к уроку (HomeworkEditor)
- [x] Ученик: форма отправки ДЗ (текст + файлы → Object Storage)
- [x] Ученик: видит статус ДЗ и фидбек куратора в карточке урока
- [x] Куратор / Admin: список ДЗ на проверку (`/curator/homework`)
- [x] Куратор / Admin: просмотр работы, текстовый комментарий с обратной связью
- [x] Admin: AdminShell на `/curator/*` маршрутах (сайдбар не пропадает)
- [x] Admin: реальная статистика на дашборде (студенты, курсы, ДЗ, прогресс)
**Доработки (добавить в рамках Этапа 9):**
- [ ] Фильтры в списке ДЗ: по имени/email ученика, по уроку, по курсу, по статусу
- [ ] Поиск по имени/email ученика
- [ ] Пагинация + выбор кол-ва записей на странице (20/50/100)
- [ ] Расширенные статусы: «Новое» / «Просмотрено» / «Прокомментировано»
- [ ] Шаблоны ответов куратора — готовые тексты фидбека, выбираемые из списка
---
## Этап 6 — Обсуждения под уроками ✅ ЗАВЕРШЁН (07.04.2026)
- [x] Prisma: LessonComment (soft-delete через поле `deleted`)
- [x] Рендер списка комментариев под уроком (server-fetched, client-rendered)
- [x] Форма отправки комментария (только для enrolled учеников и admin)
- [x] Модерация: автор, куратор или admin может удалить комментарий
- [x] Счётчик активных комментариев в заголовке секции
**Не реализовано (добавить в Этап 9 или отдельно):**
- [ ] `/admin/comments` — сводная таблица всех комментариев по всем урокам
- Колонки: №, Имя ученика, Урок (ссылка), Время, кол-во ответов, превью текста
- Удалить комментарий прямо из списка
- Пагинация
- Ссылка в сайдбаре AdminNav
---
## Этап 7 — Email-уведомления ✅ ЗАВЕРШЁН (07.04.2026)
- [x] Базовый HTML email-шаблон (фирменный стиль Second Brain)
- [x] Приветственное письмо при регистрации (`databaseHooks.user.create.after`)
- [x] Письмо ученику об открытии доступа к курсу
- [x] Куратор / Admin: уведомление о новом ДЗ на проверку
- [x] Ученик: уведомление о полученном фидбеке
- [x] Resend domain: mailsend.second-brain.ru (verified)
---
## Этап 8 — Импорт уроков из Markdown (Obsidian) ✅ ЗАВЕРШЁН (07.04.2026)
- [x] API: `POST /api/admin/import-md` — принимает .md-файл
- [x] Парсинг frontmatter (title, kinescopeId, order, published) через `gray-matter`
- [x] Конвертация Markdown → TipTap JSON через `unified` + `remark-parse`
- [x] Поддержка: заголовки, параграфы, жирный/курсив/зачёркнутый, инлайн-код, блоки кода, цитаты, списки, ссылки, изображения (HTTP), горизонтальные разделители
- [x] Очистка Obsidian-синтаксиса: `![[image]]` удаляется, `[[link|alias]]` → текст
- [x] UI: кнопка «Импорт .md» в редакторе урока — заполняет форму без автосохранения
---
## Этап 9 — Настройки платформы (Admin Settings) ✅ ЗАВЕРШЁН (08.04.2026)
**Цель:** администратор управляет ключевыми параметрами платформы без правки кода.
### Основное
- [ ] Название школы (используется в заголовке сайта, подписи писем)
- [ ] Описание школы (мета-тег description)
- [ ] Ключевые слова (мета-тег keywords)
- [ ] Режим тех. работ: вкл/выкл (показывает заглушку всем кроме admin)
- [ ] Регистрация учеников: вкл/выкл
### Оформление
- [ ] Логотип школы (загрузка → Object Storage, отображается в шапке)
- [ ] Фавикон (загрузка → Object Storage)
- [ ] Показывать логотип: да/нет
### Уведомления
- [ ] Email(ы) для системных уведомлений (кому слать письма о ДЗ, вопросах, регистрациях)
- [ ] Уведомление куратору/админу о новом ДЗ: вкл/выкл
- [ ] Уведомление куратору/админу о новом вопросе ученика: вкл/выкл
- [ ] Уведомление админу о новой регистрации: вкл/выкл
- [ ] Уведомление ученику при ответе на ДЗ/вопрос: вкл/выкл
### Данные ученика
- [ ] Требовать подтверждение email перед доступом к курсам: да/нет
- [ ] Фамилия при регистрации: обязательная / необязательная / выключена
- [ ] Телефон при регистрации: обязательный / необязательный / выключен
### Защита
- [ ] Одна активная сессия на аккаунт: вкл/выкл
- [ ] CAPTCHA на форме регистрации: вкл/выкл (reCAPTCHA v3)
### Права куратора
- [ ] Куратор видит ДЗ: по всем курсам / только по назначенным курсам
- [ ] Куратор может отвечать на вопросы учеников: да/нет
- [ ] Куратор видит список всех студентов: да/нет
### Вставка кода
- [ ] Произвольный код в `<head>` (Yandex.Metrika, Google Analytics, пиксели)
- [ ] Произвольный код в `<body>` (виджеты, чаты поддержки)
### Юридические документы
- [ ] URL Политики конфиденциальности (ссылка на внешний документ)
- [ ] URL Согласия на обработку персональных данных
- [ ] URL Договора-оферты
- [ ] Показывать чекбокс «Я принимаю условия» при регистрации: да/нет
- [ ] Реквизиты организации (текстовое поле, отображается в подвале)
### Соц. сети
- [ ] YouTube: одна ссылка
- [ ] VK: несколько ссылок (название + URL), например «Основная группа» и «Канал»
- [ ] Telegram: несколько ссылок (название + URL), например «Основной канал» и «Канал курса»
(отображаются в подвале личного кабинета ученика; хранятся как JSON-массив в Settings)
### Вопросы учеников
- [ ] Система вопросов глобально: вкл/выкл
- [ ] Куратор/админ может написать ученику первым: да/нет
- [ ] Вопросы только по курсам ученика: да/нет
- [ ] Включать вопросы для новых курсов автоматически: да/нет
**Хранение:** таблица `Settings` (key-value), доступна через `getSettings()` в server components.
**Критерий готовности:** меняю название школы → оно появляется в заголовке. Включаю тех. работы → ученики видят заглушку. Куратор привязан к курсу — видит только его ДЗ.
---
## Этап 11 — Импорт/Экспорт учеников и миграция с emdesell
**Цель:** все пользователи и контент перенесены в новую LMS. Раздел `/admin/import-export`.
### Импорт учеников (CSV)
- [ ] Скачать файл-шаблон CSV (Email, Имя, Фамилия, Телефон)
- [ ] Загрузка CSV, поддержка кодировок Windows-1251 и UTF-8
- [ ] Опция: подтверждать email автоматически (да/нет)
- [ ] Опция: обновлять уже существующие аккаунты (да/нет)
- [ ] Присвоение доступов к курсам при импорте (выбор курса + срок в днях, 0 = бессрочно)
- [ ] Опция: отправить письмо-уведомление ученику (со ссылкой для установки пароля)
- [ ] Предпросмотр перед применением (таблица: кто создаётся, кто обновляется, кому даётся доступ)
- [ ] Применить импорт — создать пользователей, выдать доступы, отправить письма
### Экспорт учеников (CSV)
- [ ] Все ученики или фильтр по конкретному курсу/доступу
- [ ] Фильтр по просмотрам уроков (экспортировать только тех кто смотрел)
- [ ] Выбор кодировки: Windows-1251 (для Excel) / UTF-8
- [ ] Поля: Email, Имя, Фамилия, Телефон, Дата регистрации, Курсы, Прогресс
### Миграция контента
- [ ] Чек-лист ручного переноса контента (уроки, PDF, структура курсов)
- [ ] Скрипт проверки целостности: все enrolled пользователи имеют доступ к нужным курсам
- [ ] QA: проверить 10 случайных аккаунтов после импорта
**Критерий готовности:** загружаю CSV из emdesell → предпросмотр показывает корректные данные → применяю → ученики получают письма → могут войти и продолжить обучение.
---
## Этап 12 — Telegram-бот и аналитика
**Цель:** уведомления в Telegram для всех участников, базовая аналитика.
**Настройки (в разделе Настройки → Telegram):**
- Токен бота (вводится в админке, хранится в Settings)
- Интеграция вкл/выкл глобально
- Показывать кнопку «Подключить Telegram» в кабинете ученика: да/нет
**Уведомления куратору/админу:**
- [ ] Новое ДЗ на проверку
- [ ] Новый вопрос от ученика
- [ ] Новая регистрация студента
- [ ] Ошибки платформы (500-е, failed email и т.д.)
**Уведомления ученику:**
- [ ] Получен фидбек по ДЗ
- [ ] Ответ куратора на вопрос
- [ ] Открыт доступ к новому курсу
**Реализация:**
- [ ] Ученик привязывает Telegram через `/start` в боте (сохраняется `telegramChatId` в User)
- [ ] Кнопка «Подключить Telegram» в личном кабинете ученика
- [ ] Админ/куратор вводит свой `chatId` в профиле или через `/start`
- [ ] Настройки бота в разделе Настройки → Telegram
- [ ] Yandex.Metrika: базовое подключение (pageviews)
- [ ] Admin: простая страница аналитики (активные ученики, прогресс по курсам)
---
## Этап 13 — Тесты и квизы
**Цель:** можно добавить тест к уроку, ученик проходит и получает результат. **Цель:** можно добавить тест к уроку, ученик проходит и получает результат.
- [ ] Prisma: Quiz, QuizQuestion, QuizOption, QuizAttempt - [ ] Prisma: Quiz, QuizQuestion, QuizOption, QuizAttempt (схема уже есть)
- [ ] Admin: создание теста к уроку (добавить вопрос → варианты ответов → отметить правильный) - [ ] Admin: создание теста к уроку (вопрос → варианты → отметить правильный)
- [ ] Типы вопросов: одиночный выбор, множественный выбор, короткий текст - [ ] Типы вопросов: одиночный выбор, множественный выбор, короткий текст
- [ ] Рендер теста в уроке для ученика - [ ] Рендер теста в уроке для ученика
- [ ] Авто-проверка (single/multiple choice), результат сразу - [ ] Авто-проверка (single/multiple choice), результат сразу
@@ -87,105 +273,34 @@
--- ---
## Этап 5 — Домашние задания и обратная связь куратора
**Цель:** ученик сдаёт ДЗ, куратор оставляет комментарий.
- [ ] Prisma: Homework, HomeworkSubmission, HomeworkFeedback
- [ ] Admin: добавить блок ДЗ к уроку (текст задания)
- [ ] Ученик: форма отправки ДЗ (текст + файлы → Object Storage)
- [ ] Ученик: ДЗ засчитывается **автоматически при отправке** (урок открывается сразу)
- [ ] Куратор: список ДЗ на проверку (все или по курсу), статус «новое / просмотрено»
- [ ] Куратор: просмотр ДЗ, оставить текстовый комментарий
- [ ] История обмена по ДЗ (ученик может ответить на комментарий куратора)
- [ ] Уведомление ученику когда куратор оставил комментарий (заглушка — реальные email на Этапе 7)
**Критерий готовности:** отправляю ДЗ — урок открывается. Куратор заходит в панель, видит ДЗ, оставляет комментарий. Ученик видит комментарий в карточке урока.
---
## Этап 6 — Обсуждения под уроками
**Цель:** ученики могут общаться под каждым уроком.
- [ ] Prisma: LessonComment (с поддержкой вложенных ответов — опционально)
- [ ] Рендер треда комментариев под уроком
- [ ] Форма отправки комментария (только для enrolled учеников)
- [ ] Модерация: куратор/админ может удалить комментарий
- [ ] Пагинация или infinite scroll для длинных тредов
**Критерий готовности:** ученик оставляет комментарий, другой ученик его видит, куратор может удалить.
---
## Этап 7 — Email-уведомления
**Цель:** все участники получают нужные письма через Resend.
- [ ] Базовый email-шаблон (HTML, фирменный стиль)
- [ ] Ученик: подтверждение регистрации (уже частично с Этапа 0, финализировать)
- [ ] Ученик: письмо когда куратор оставил комментарий к ДЗ
- [ ] Ученик: письмо когда ответили на его комментарий в уроке
- [ ] Куратор / Админ: новое ДЗ на проверку
- [ ] Куратор / Админ: новый комментарий в обсуждении
- [ ] Админ: зарегистрирован новый ученик
- [ ] Очередь отправки (edge case: Resend временно недоступен → retry)
**Критерий готовности:** отправляю ДЗ — куратор получает email. Куратор отвечает — ученик получает email.
---
## Этап 8 — Импорт уроков из Markdown
**Цель:** могу импортировать урок из .md-файла Obsidian одним действием.
- [ ] API: `POST /api/admin/lessons/import-md` — принимает .md-файл
- [ ] Парсинг frontmatter (title, order, kinescopeId и кастомные поля) → метаданные урока
- [ ] Конвертация Markdown-тела в TipTap JSON (через remark / rehype)
- [ ] UI в админке: кнопка «Импортировать из .md» на странице урока
- [ ] Обработка картинок в Markdown (локальные пути → Object Storage)
**Критерий готовности:** беру .md-файл из Obsidian с frontmatter и текстом → импортирую → урок создан с правильными метаданными и контентом.
---
## Этап 9 — Миграция с emdesell
**Цель:** все пользователи и контент перенесены в новую LMS.
- [ ] Скрипт импорта пользователей из CSV-экспорта emdesell (email, имя, курсы)
- [ ] Создание пользователей без пароля + письмо «установите пароль»
- [ ] Назначение доступов к курсам по данным из CSV
- [ ] Чек-лист ручного переноса контента (уроки, PDF, структура курсов)
- [ ] Скрипт проверки целостности: все enrolled пользователи имеют доступ к нужным курсам
- [ ] QA: проверить 10 случайных аккаунтов после импорта
**Критерий готовности:** все ученики из emdesell могут войти в новую LMS и продолжить обучение.
---
## Этап 10 — Telegram-бот и аналитика
**Цель:** получаю уведомления в Telegram, вижу базовую аналитику.
- [ ] Telegram-бот: уведомление куратору/админу о новом ДЗ
- [ ] Telegram-бот: уведомление об ошибках (500-е, failed email и т.д.)
- [ ] Yandex.Metrika: базовое подключение (pageviews)
- [ ] Admin: простая страница аналитики (активные ученики, прогресс по курсам)
---
## Этап 11 — Деплой на Hetzner
**Цель:** LMS работает на production-сервере по своему домену с SSL.
- [ ] `docker-compose.prod.yml`: app + PostgreSQL + Redis + Nginx
- [ ] Nginx: SSL через Let's Encrypt (certbot), reverse proxy на Next.js
- [ ] GitHub Actions: CI/CD pipeline (lint → build → push Docker image → deploy)
- [ ] Резервное копирование PostgreSQL (cron → Object Storage)
- [ ] Мониторинг uptime (UptimeRobot или аналог)
- [ ] `.env` на сервере через Hetzner Secrets Manager или vault-файл вне репозитория
- [ ] Smoke-тест: регистрация → урок → ДЗ → куратор → email
**Критерий MVP готов:** создаю курс из админки, добавляю уроки с Kinescope, импортирую ученика из emdesell, даю доступ — ученик регистрируется, проходит урок, сдаёт тест, отправляет ДЗ, получает автодоступ к следующему уроку, позже — комментарий куратора на email.
---
## Бэклог (после MVP) ## Бэклог (после MVP)
- **Миграция email-шаблонов на React Email 6 + Resend CLI 2.0** (Resend Launch Week 6, 24.04.2026):
- React Email 6: новые шаблоны для auth и ecommerce flows (welcome, password reset, purchase confirmation, course progress) — можно взять за основу вместо своих
- Resend CLI 2.0: локальный preview и тестирование шаблонов (`resend send --local ...`), 50+ команд
- Embeddable open-source editor (в одну строку) — отложить, пока не требуется
- Сейчас Этап 7 (Email-уведомления) завершён на базовой связке, задача — рефакторинг на React Email
- **Самостоятельная регистрация + автоматический онбординг** — два сценария входа и воронка после регистрации:
**Сценарии регистрации:**
- С лендинга через покупку — пользователь оплачивает курс, аккаунт создаётся автоматически, письмо с доступом приходит сразу
- Прямой вход на платформу — пользователь приходит по реферальной ссылке, из соцсетей, от партнёров — регистрируется сам без покупки
**Автоматический онбординг после регистрации:**
- Автоназначение вводных / вотер-модулей курсов (бесплатные превью, чтобы зацепить)
- Доступ к базовой библиотеке материалов по умолчанию (статьи, шаблоны, гайды — определяется в настройках)
- Приветственная воронка: серия писем / уведомлений, которая ведёт к первой покупке
- Уведомление администратора о новой регистрации (email + Telegram)
**Что нужно проработать:**
- Публичная страница регистрации (+ CAPTCHA, опционально)
- Настройка в Этапе 9: «Регистрация открыта: да/нет» + выбор вводных курсов/модулей, которые назначаются автоматически
- Интеграция с платёжной системой: оплата на лендинге → автосоздание аккаунта → автовыдача доступа к купленному курсу
- Разграничение: что видит гость / зарегистрированный без покупки / купивший курс
- Резервное копирование PostgreSQL (cron → Object Storage)
- GitHub Actions: CI/CD pipeline (lint → build → push Docker image → deploy)
- Сертификаты по окончании курса - Сертификаты по окончании курса
- Геймификация (баллы, бейджи, рейтинги) - Геймификация (баллы, бейджи, рейтинги)
- Промокоды и интеграция с платёжными системами - Промокоды и интеграция с платёжными системами
@@ -193,3 +308,35 @@
- Kinescope DRM (signed URLs) — при переходе на платный план - Kinescope DRM (signed URLs) — при переходе на платный план
- Водяные знаки на PDF и картинках - Водяные знаки на PDF и картинках
- Мобильное приложение - Мобильное приложение
- **Вопросы учеников** — система тикетов `/admin/questions` и `/questions` для ученика:
- Таблица в админке: №, Имя, Курс, Тема, Статус (Ожидает / Отвечено), Дата
- Статусы отсортированы: сначала «Ожидает ответа»
- Куратор/Admin может создать обращение первым (написать ученику)
- Внутри тикета: история переписки, смена статуса
- **База знаний** — FAQ, который ученик видит до отправки вопроса
- **Шаблоны ответов** — куратор выбирает готовый ответ из списка
- Email + Telegram уведомления обеим сторонам
- **Главная страница ученика** — кастомизируемый экран после входа:
- Приветственный баннер с описанием школы (редактируется в настройках)
- Список курсов ученика с прогрессом
- Блок бесплатных/открытых материалов (статьи, PDF, видео)
- Анонсы ближайших событий и новых курсов
- **Медиатека (Файлы)** — централизованное файловое хранилище `/admin/files`:
- Prisma: `MediaFolder` (id, name, courseId?, createdAt) + `MediaFile` (id, folderId?, name, url, size, mimeType, uploadedById, createdAt)
- Папки автоматически создаются по курсам + «Common» для общих файлов
- Вид: грид (карточки с иконкой типа) или список — переключатель
- Breadcrumb-навигация: Все файлы / Название папки
- Загрузка файлов (PDF, изображения, любые) → Object Storage
- Создание папки вручную
- Клик на файл → диалог: имя (редактируемое), дата загрузки, размер, автор
- Действия в диалоге: скопировать ссылку, скачать, удалить
- Вставка файлов из медиатеки в урок (вместо повторной загрузки)
- **Цифровой сад** — публичный раздел платформы для сообщества:
- Методические материалы и статьи (PKM, Obsidian, Second Brain)
- Рекомендованная литература с аннотациями
- Записи открытых встреч и вебинаров
- Календарь: предстоящие открытые уроки, запуски курсов, события
- Возможно: публичный Obsidian-like граф знаний
+267
View File
@@ -0,0 +1,267 @@
# 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=<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/<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
### Этап 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
+153
View File
@@ -0,0 +1,153 @@
# Tech Debt Audit — lms-sb
Generated: 2026-05-09
---
## Executive Summary
- **1 Critical**: middleware не работает — файл называется `proxy.ts` вместо `middleware.ts`, защита маршрутов на уровне Next.js отсутствует
- **3 High**: двойная загрузка полной структуры курса на каждый урок; `getSettings()` вызывается дважды в root layout; нет ограничения размера загружаемых файлов
- **0 тестов** — ни одного test-файла во всём проекте
- **4 отладочных `console.log`** в production-коде Server Action
- Zod установлен как зависимость, но нигде не используется — Server Actions принимают `FormData` без валидации схемы
- 3 уязвимости npm **high** severity (next, fast-uri, fast-xml-builder)
- `settings-form.tsx` — 506 строк, единственный god-файл, но внутренняя структура оправданна
- Самые горячие файлы совпадают с самыми крупными: student lesson page (12 правок, 270 строк) и lesson-editor (8 правок, 408 строк) — концентрация долга
---
## Architectural Mental Model
LMS построена на Next.js 16 App Router с тремя зонами доступа: `(auth)`, `(student)`, `admin`/`curator`. Мутации идут через Server Actions, данные читаются в RSC. Better Auth отвечает за сессии и роли. Prisma 7 с PostgreSQL через собственный PrismaPg адаптер (обходит ограничения Turbopack).
Главная аномалия: middleware объявлен в `src/proxy.ts` с функцией `proxy()`, а не в `src/middleware.ts` с функцией `middleware()`. Next.js его не подхватывает. Защита работает только за счёт явных проверок сессии в каждой странице и action — что само по себе достаточно, но заявленный в CLAUDE.md "Auth middleware (защита маршрутов)" фактически не существует.
Второй структурный факт: при открытии страницы урока студентом происходит двойная загрузка полной структуры курса — один раз в `layout.tsx` (для sidebar), второй в `page.tsx` (для prev/next навигации). Это N+1 на уровне layout/page.
---
## Findings
| ID | Category | File:Line | Severity | Effort | Description | Recommendation |
|----|----------|-----------|----------|--------|-------------|----------------|
| F001 | Security | src/proxy.ts:1 | Critical | S | Файл `proxy.ts` не является Next.js middleware. Next.js ищет `src/middleware.ts` с экспортом `middleware`. Маршруты не защищены на уровне edge. | Переименовать файл в `src/middleware.ts`, переименовать экспорт `proxy``middleware` |
| F002 | Performance | src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx:37 | High | M | `page.tsx` загружает `lesson.module.course.modules` с вложенными уроками для prev/next nav — та же структура уже загружена в `layout.tsx:20`. Двойной DB-запрос на каждый pageview урока. | Вынести prev/next навигацию в layout или передавать через `searchParams`/context вместо повторной загрузки |
| F003 | Performance | src/app/layout.tsx:14,27 | High | S | `getSettings()` вызывается дважды в одном компоненте — в `generateMetadata()` и в `RootLayout()`. Два одинаковых DB-запроса на каждый запрос. | Объединить в один вызов или обернуть `getSettings` в `React.cache()` |
| F004 | Security | src/app/api/admin/upload/route.ts:1, src/app/api/student/homework-upload/route.ts:1, src/app/api/curator/audio-upload/route.ts:1 | High | S | Ни один upload endpoint не проверяет размер файла перед `file.arrayBuffer()`. Загрузка 1 ГБ файла ляжет в память Node.js. | Добавить проверку `file.size` до 50 МБ (или другого лимита) сразу после `form.get("file")` |
| F005 | Observability | src/lib/actions/lesson-actions.ts:24,27,42,48 | Medium | S | 4 `console.log` в production Server Action. Логируют `lessonId` и статус каждого сохранения в prod-консоль. | Убрать все 4. Оставить только `console.error` в catch-блоках. |
| F006 | Type & Contract | src/app/admin/courses/actions.ts:28-47 | Medium | M | Server Actions принимают `FormData` и читают поля через `as string` без валидации. Zod установлен, но не используется нигде в проекте. | Добавить Zod-схему на входе `createCourse` и `updateCourse`; повторить паттерн в остальных actions |
| F007 | Dependencies | package.json | Medium | S | `npm audit` показывает 3 high severity уязвимости: `next` (сам фреймворк), `fast-uri`, `fast-xml-builder`. | `npm update next` до последнего патча; проверить влияние на остальные зависимости |
| F008 | Dependencies | package.json | Low | S | `depcheck` находит 4 неиспользуемые зависимости: `@tailwindcss/typography`, `shadcn`, `tw-animate-css`, `zod`. Если Zod будет использован (F006), убрать оставшиеся три. | `npm remove @tailwindcss/typography shadcn tw-animate-css` |
| F009 | Architecture | src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx:102-106 | Low | S | Функция `formatSize` определена внутри Server Component — при каждом рендере пересоздаётся. Не ошибка, но засоряет файл. | Вынести в `src/lib/utils.ts` |
| F010 | Consistency | src/app/admin/courses/[courseId]/actions.ts:12, src/app/admin/categories/actions.ts:11, src/app/admin/settings/actions.ts:11 | Low | S | Разные строки ошибки авторизации: `"Forbidden"` (EN), `"Нет доступа"` (RU), `"Unauthorized"` (EN). Нет единого паттерна. | Выбрать один формат и применить везде; ошибки авторизации не должны уходить клиенту как читаемый текст |
| F011 | Security | src/app/layout.tsx:32,37 | Low | — | `dangerouslySetInnerHTML` с `headCode`/`bodyCode` из БД — admin может вставить произвольный JS. | Намеренная функция (code injection для аналитики). Задокументировать явно, что это admin-only привилегия. Добавить проверку роли на странице настроек — уже есть. |
| F012 | Testing | — | High | L | Ни одного теста во всём проекте. Нет `*.test.*`, `*.spec.*`, нет `__tests__/`. Горячие файлы (lesson page, lesson-editor) не прикрыты ничем. | Начать с unit-тестов `src/lib/md-to-tiptap.ts` (чистая функция, высокий риск регрессии) и `src/lib/settings.ts`. Для UI — Playwright E2E на login + lesson complete flow. |
| F013 | Consistency | src/app/(student)/courses/[slug]/layout.tsx:54, src/app/(student)/courses/[slug]/lessons/[lessonId]/page.tsx:53 | Low | S | Оба файла самостоятельно проверяют `isAdmin` для conditional DB queries с одинаковой логикой. Паттерн не вынесен. | Не критично при текущем размере, но при росте числа маршрутов станет проблемой |
| F014 | Documentation | src/proxy.ts:1, CLAUDE.md | Medium | S | CLAUDE.md: `src/middleware.ts — Auth middleware (защита маршрутов)`. Файла не существует, существует `src/proxy.ts`. Документация врёт. | После исправления F001 — обновить CLAUDE.md |
---
## Top 5 "fix these first"
### 1. F001 — Переименовать proxy.ts в middleware.ts
```bash
git mv src/proxy.ts src/middleware.ts
```
В `src/middleware.ts`:
```ts
// было:
export function proxy(request: NextRequest) { ... }
// стало:
export function middleware(request: NextRequest) { ... }
```
`config` экспорт уже правильный — оставить как есть. Это однострочный фикс с нулевым риском регрессии.
---
### 2. F003 — Двойной `getSettings()` в root layout
```ts
// src/app/layout.tsx — было: два вызова
const settings = await getSettings(); // в generateMetadata
const settings = await getSettings(); // в RootLayout
// стало: обернуть в React.cache
// src/lib/settings.ts
import { cache } from "react";
export const getSettings = cache(async (): Promise<Settings> => { ... });
```
Один `cache()` снимает оба дублированных запроса в рамках одного render pass.
---
### 3. F004 — Лимит размера файлов в upload endpoints
В каждом из 5 upload routes добавить сразу после получения файла:
```ts
const MAX_BYTES = 50 * 1024 * 1024; // 50 МБ
if (file.size > MAX_BYTES) {
return NextResponse.json({ error: "Файл слишком большой" }, { status: 413 });
}
```
---
### 4. F005 — Убрать console.log из lesson-actions.ts
```ts
// src/lib/actions/lesson-actions.ts — удалить строки 24, 27, 42, 48
console.log("[saveLesson] start", lessonId); // удалить
console.log("[saveLesson] auth ok"); // удалить
console.log("[saveLesson] db update ok"); // удалить
console.log("[saveLesson] done"); // удалить
```
---
### 5. F002 — Устранить двойную загрузку структуры курса
`layout.tsx` уже загружает все модули/уроки курса для sidebar. `page.tsx` загружает ту же структуру ещё раз для prev/next навигации. Самое чистое решение — передавать `allLessons` через `searchParams` или вычислять в layout и передавать через `slot`:
Альтернатива проще: убрать из `page.tsx` `include: { modules: { include: { lessons } } }` и принять `prevLessonId`/`nextLessonId` как query params, которые layout прописывает в ссылки sidebar-а.
---
## Quick Wins
- [ ] **F001**`git mv src/proxy.ts src/middleware.ts` + переименовать экспорт (5 минут)
- [ ] **F003**`import { cache } from "react"` в settings.ts, обернуть `getSettings` (10 минут)
- [ ] **F005** — Удалить 4 строки `console.log` в lesson-actions.ts (2 минуты)
- [ ] **F007**`npm update next` — закрыть CVE в самом фреймворке
- [ ] **F008**`npm remove @tailwindcss/typography shadcn tw-animate-css` — убрать мёртвый вес
- [ ] **F004** — Добавить `file.size` проверку в 5 upload routes (15 минут)
---
## Things that look bad but are actually fine
**`src/generated/prisma/`** — 20+ сгенерированных файлов в `src/`. Выглядит как мусор, но это намеренно: Prisma 7 с Turbopack требует TypeScript-клиента в src для корректной работы RSC. Объяснено в коммите `af8644e` и в memory-файле `project_lms_prisma_config.md`. Не трогать.
**`src/app/(student)/courses/[slug]/layout.tsx:54`** — `prisma.lessonProgress.findMany({ where: { lessonId: { in: allLessonIds } } })` загружает прогресс по всем урокам курса. Выглядит избыточно, но это единственный способ отрисовать sidebar с чекбоксами без N+1 запроса на каждый урок. Правильный паттерн.
**`src/lib/auth.ts:37`** — Хардкод `"https://school.second-brain.ru"` в `trustedOrigins`. Выглядит как нарушение правила "нет захардкоженных URL". На самом деле это security-критичный список и он должен быть явным, не конфигурируемым через переменные (иначе можно было бы переопределить в .env). Оставить.
**`catch (() => {})` в трёх местах** (email в import, S3 delete) — выглядит как проглатывание ошибок. В контексте это правильно: ошибка отправки welcome-email или удаления старого файла из S3 не должна ронять основную операцию (импорт/апгрейд файла).
**506 строк в `settings-form.tsx`** — формально god-файл, но это один большой конфиг-экран с однородной структурой (Section + Field + Toggle). Файл читается линейно, нет запутанной логики. Декомпозиция не добавит ясности.
---
## Open Questions
1. **`forgot-password` и `reset-password` маршруты** не входят в `PUBLIC_ROUTES` в `proxy.ts:4`. Это намеренно — эти страницы требуют cookie для валидации токена? Или просто забыто?
2. **`src/app/api/admin/import-md/route.ts`** — существует, но нет UI для вызова. Мёртвый endpoint или WIP?
3. **QuizOption** — схема Prisma содержит `QuizOption` с `isCorrect`, но в `page.tsx` урока quiz загружается без `options` (`include: { questions: { orderBy: { order: "asc" } } }`). Тест работает только с open-ended вопросами или options загружаются где-то ещё?
4. **`load-test.js`** в корне репо — `k6` отмечен как missing dependency в depcheck. Это намеренно отдельный инструмент или планируется CI-интеграция?
+25
View File
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
@@ -0,0 +1,159 @@
# Student Questions — Design Spec
_Created: 20260519_
## Overview
A support-chat feature inside the LMS. Students ask questions, school staff (admin/curator) answers. Each question is a threaded conversation with open/closed status. Includes file attachments and email notifications for all parties.
Also in scope: email notifications for homework submissions (new + student updates).
---
## Data Model
### StudentQuestion
```
id String @id @default(cuid())
userId String -- student who created it
courseId String? -- optional course context
title String
status QuestionStatus @default(OPEN) -- OPEN | CLOSED
closedAt DateTime?
closedById String? -- admin/curator who closed
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User
course Course?
messages StudentQuestionMessage[]
```
### StudentQuestionMessage
```
id String @id @default(cuid())
questionId String
authorId String
text String
files String[] -- S3 paths: questions/{questionId}/{messageId}/{filename}
isRead Boolean @default(false)
createdAt DateTime @default(now())
question StudentQuestion
author User
```
### QuestionStatus (enum)
```
OPEN
CLOSED
```
---
## Routes
### Student
| Route | Description |
|---|---|
| `(student)/questions` | List of own questions with unread indicators |
| `(student)/questions/new` | Form to create a new question |
| `(student)/questions/[id]` | Thread view — read messages + reply |
### Admin / Curator
| Route | Description |
|---|---|
| `admin/questions` | Split-view: question list left, thread right |
| `curator/questions` | Same split-view (curators have same access as admins here) |
---
## UI Behaviour
### Student — Questions List (`/questions`)
- Header: "Мои вопросы" + "+ Задать вопрос" button (→ `/questions/new`)
- Each row: title, message count, last activity, status badge (ОТКРЫТ / ЗАКРЫТ)
- Unread indicator: black dot + "Новый ответ от школы" when school replied since last student visit
- Closed questions: dimmed (opacity 0.7), grey badge
- Active (unread) questions: bold border, bold title
### Student — Thread (`/questions/[id]`)
- Header: question title, created date, status, "← Все вопросы" link
- Message bubbles: student messages left (#E8E8E0), school messages right (#F5F5F0 + green border #E8F0D8)
- New school message: bold label "🔵 новое" in timestamp
- File attachments shown inline under message text (📎 filename · size)
- Reply form at bottom: textarea + attach button + send button
- Attachment types: jpg, png, pdf, md · max 10 MB
- Student CAN reply to closed questions (creates new message, does NOT reopen question)
### Admin/Curator — Split View (`/admin/questions`, `/curator/questions`)
- Left panel (45%): tab filter "Открытые / Закрытые", question list sorted by last activity
- Unread: red dot, green-tinted background, bold student name
- Right panel (55%): selected thread with full message history + reply form + "✓ Закрыть вопрос" button
- Only admin/curator can close a question
- Closing a question does NOT prevent further messages
---
## Notifications
### → School (admin + all curators)
| Trigger | Channel |
|---|---|
| New question created | Email + admin sidebar badge (count of unread questions) |
| Student adds message to existing question | Email |
### → Student
| Trigger | Channel |
|---|---|
| Admin/curator replies to question | Email |
### Admin Badge
- Sidebar badge shows count of questions with unread messages (school hasn't seen yet)
- Separate from homework badge
### Email for Homework (added to scope)
| Trigger | Recipient |
|---|---|
| New HomeworkSubmission created | Admin + all curators |
| Student updates existing submission (adds text/file) | Admin + all curators |
---
## File Storage
- Path pattern: `questions/{questionId}/{messageId}/{filename}`
- Reuse existing S3 upload infrastructure (`src/lib/s3.ts`)
- Allowed: jpg, png, pdf, md
- Max size: 10 MB per file
- No limit on number of files per message (reasonable: 5)
---
## Read Tracking
- `isRead` flag per message, set to `true` when the OTHER party opens the thread
- Student opens `/questions/[id]` → all school messages in that thread marked `isRead = true`
- Admin/curator opens a question in split-view → all student messages marked `isRead = true`
- Admin badge recalculates on each page load (count questions where latest student message is unread)
---
## API Routes
```
POST /api/questions -- create question
GET /api/questions -- list own questions (student) or all (admin/curator)
GET /api/questions/[id] -- get question + messages
POST /api/questions/[id]/messages -- add message + files
PATCH /api/questions/[id]/close -- close question (admin/curator only)
POST /api/upload/question-file -- upload attachment, returns S3 path
```
---
## Out of Scope (this iteration)
- Question categories / tags
- Assigning question to a specific curator
- Email threading (reply-to email to answer)
- Push/browser notifications
- Question templates
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,451 @@
# LMS Staging Environment Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Развернуть полноценный staging-стенд LMS на `staging.school.second-brain.ru` — изолированный от prod, с автоматическим применением миграций и отключёнными email-уведомлениями.
**Architecture:** Hetzner VPS (178.104.27.196, 16 GB RAM), отдельный Docker Compose стек в `/root/digital-household/lms-staging/` с собственным postgres-контейнером. Caddy на том же сервере роутит `staging.school.second-brain.ru → localhost:3011`. Сборочный репо — `/root/lms-staging-build/`, тег образа — `lms-staging-app:latest`. Деплой в staging производится отдельным скриптом `deploy-staging.sh` независимо от prod.
**Tech Stack:** Docker Compose, Next.js 16 (App Router), PostgreSQL 16, Caddy, Gitea (git.second-brain.ru/admins/lms-sb)
---
## Файловая структура
```
Hetzner VPS (root@178.104.27.196):
├── /root/lms-staging-build/ ← git clone (сборочная директория)
│ └── [полный репо lms-sb]
├── /root/digital-household/lms-staging/
│ ├── docker-compose.yml ← создаём (app + db)
│ └── .env ← создаём (staging-специфичный)
└── /root/digital-household/caddy-proxy/
└── Caddyfile ← добавляем блок staging
Mac:
└── ~/Documents/Claude/scripts/deploy-staging.sh ← создаём
```
---
## Task 1: Клонировать репо на Hetzner
**Files:**
- Создаём: `/root/lms-staging-build/` на сервере
- [ ] **Шаг 1.1: SSH на Hetzner и клонировать репо**
Нужен токен Gitea. Он хранится в уже существующем git remote на сервере. Достанем его:
```bash
ssh root@178.104.27.196 "git -C /root/digital-household/lms-sb remote get-url origin 2>/dev/null || echo 'no-remote'"
```
Если remote есть — берём URL из него. Если нет — токен в `~/.config/secrets/` на Mac (найти файл с `GITEA_TOKEN` или `git.second-brain.ru`).
- [ ] **Шаг 1.2: Клонировать**
```bash
GITEA_URL=$(ssh root@178.104.27.196 "git -C /root/digital-household/lms-sb remote get-url origin 2>/dev/null")
# Пример: https://admins:TOKEN@git.second-brain.ru/admins/lms-sb.git
ssh root@178.104.27.196 "git clone ${GITEA_URL} /root/lms-staging-build"
```
- [ ] **Шаг 1.3: Проверить**
```bash
ssh root@178.104.27.196 "ls /root/lms-staging-build/src && git -C /root/lms-staging-build log --oneline -3"
```
Ожидаем: список файлов и последние 3 коммита.
---
## Task 2: Создать staging docker-compose.yml
**Files:**
- Создаём: `/root/digital-household/lms-staging/docker-compose.yml`
- [ ] **Шаг 2.1: Создать директорию**
```bash
ssh root@178.104.27.196 "mkdir -p /root/digital-household/lms-staging"
```
- [ ] **Шаг 2.2: Записать docker-compose.yml**
```bash
ssh root@178.104.27.196 "cat > /root/digital-household/lms-staging/docker-compose.yml" << 'COMPOSE'
services:
app:
image: lms-staging-app:latest
restart: unless-stopped
ports:
- "3011:3000"
env_file: .env
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: lms_staging_user
POSTGRES_PASSWORD: "${DB_PASSWORD}"
POSTGRES_DB: lms_staging_db
volumes:
- staging_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U lms_staging_user -d lms_staging_db"]
interval: 5s
timeout: 5s
retries: 10
volumes:
staging_postgres_data:
COMPOSE
```
- [ ] **Шаг 2.3: Проверить**
```bash
ssh root@178.104.27.196 "cat /root/digital-household/lms-staging/docker-compose.yml"
```
Ожидаем: полный YAML без артефактов heredoc.
---
## Task 3: Залить staging .env на сервер
**Files:**
- Создаём: `/root/digital-household/lms-staging/.env`
Все креды уже сгенерированы и хранятся в `~/.config/secrets/lms-staging.env` на Mac.
- [ ] **Шаг 3.1: Скопировать .env на сервер**
```bash
scp ~/.config/secrets/lms-staging.env root@178.104.27.196:/root/digital-household/lms-staging/.env
```
- [ ] **Шаг 3.2: Установить права**
```bash
ssh root@178.104.27.196 "chmod 600 /root/digital-household/lms-staging/.env"
```
- [ ] **Шаг 3.3: Проверить**
```bash
ssh root@178.104.27.196 "grep -E '^BETTER_AUTH_URL|^RESEND_API_KEY|^DB_PASSWORD' /root/digital-household/lms-staging/.env"
```
Ожидаем:
```
BETTER_AUTH_URL=https://staging.school.second-brain.ru
RESEND_API_KEY=
DB_PASSWORD=38002343e67efd057559d8f073d45892
```
---
## Task 4: Собрать Docker-образ и запустить staging
**Files:**
- Используем `/root/lms-staging-build/` для сборки
- [ ] **Шаг 4.1: Собрать образ**
```bash
ssh root@178.104.27.196 "cd /root/lms-staging-build && docker build -t lms-staging-app:latest . 2>&1 | tail -5"
```
Ожидаем: `Successfully built ...` или `naming to docker.io/library/lms-staging-app:latest done`.
Время: ~2–4 минуты.
- [ ] **Шаг 4.2: Запустить контейнеры**
```bash
ssh root@178.104.27.196 "cd /root/digital-household/lms-staging && docker compose up -d"
```
Ожидаем: `Container lms-staging-db-1 Started`, `Container lms-staging-app-1 Started`.
- [ ] **Шаг 4.3: Проверить логи (через 10 сек)**
```bash
ssh root@178.104.27.196 "sleep 10 && docker logs lms-staging-app-1 --tail 20"
```
Ожидаем в логах:
- `Running database migrations...`
- `No pending migrations to apply.` (или список применённых)
- `Starting Next.js...`
- `✓ Ready in 0ms`
Если вместо этого ошибка `DATABASE_URL` или `connection refused` — проверить `.env`: `docker exec lms-staging-app-1 env | grep DATABASE`.
---
## Task 5: Настроить Caddy
**Files:**
- Изменяем: `/root/digital-household/caddy-proxy/Caddyfile`
- [ ] **Шаг 5.1: Посмотреть текущий конец Caddyfile**
```bash
ssh root@178.104.27.196 "tail -10 /root/digital-household/caddy-proxy/Caddyfile"
```
- [ ] **Шаг 5.2: Добавить блок для staging**
```bash
ssh root@178.104.27.196 "cat >> /root/digital-household/caddy-proxy/Caddyfile" << 'CADDY'
staging.school.second-brain.ru {
reverse_proxy http://localhost:3011
}
CADDY
```
- [ ] **Шаг 5.3: Перезагрузить Caddy**
```bash
ssh root@178.104.27.196 "docker exec caddy-proxy caddy reload --config /etc/caddy/Caddyfile 2>&1"
```
Ожидаем: пустой вывод (успех) или `Successfully loaded new configuration`.
Если ошибка синтаксиса — проверить: `docker exec caddy-proxy caddy validate --config /etc/caddy/Caddyfile`.
---
## Task 6: Добавить DNS-запись
DNS для `*.school.second-brain.ru` и `school.second-brain.ru` нужно проверить — где управляется зона.
- [ ] **Шаг 6.1: Проверить текущий DNS**
```bash
dig +short staging.school.second-brain.ru
dig +short school.second-brain.ru
```
Если `staging.school.second-brain.ru` не резолвится — нужно добавить A-запись.
- [ ] **Шаг 6.2: Добавить A-запись**
В панели DNS (Hetzner DNS Console или CloudFlare — где управляется зона `second-brain.ru`):
```
Тип: A
Имя: staging.school
Значение: 178.104.27.196
TTL: 300
```
- [ ] **Шаг 6.3: Дождаться распространения**
```bash
# Через 1-2 минуты:
dig +short staging.school.second-brain.ru
```
Ожидаем: `178.104.27.196`
---
## Task 7: Проверить работу staging end-to-end
- [ ] **Шаг 7.1: HTTP проверка**
```bash
curl -s -o /dev/null -w "%{http_code}" https://staging.school.second-brain.ru/login
```
Ожидаем: `200`
- [ ] **Шаг 7.2: Войти под тестовым аккаунтом**
Открыть `https://staging.school.second-brain.ru/login` в браузере.
БД пустая, но seed создаёт admin-аккаунт. Запустить seed:
```bash
ssh root@178.104.27.196 "cd /root/lms-staging-build && docker compose -f /root/digital-household/lms-staging/docker-compose.yml exec app npx prisma db seed 2>&1 | tail -5"
```
После seed входить: `admin@second-brain.ru` / `Password123!`
- [ ] **Шаг 7.3: Проверить что email не уходит**
В staging при регистрации `/api/auth/sign-up` не должно быть ошибок Resend (только `RESEND_API_KEY` пустой — письмо тихо не отправляется, регистрация всё равно работает).
Убедиться в логах:
```bash
ssh root@178.104.27.196 "docker logs lms-staging-app-1 --since 5m 2>&1 | grep -i 'resend\|email\|error' | head -10"
```
---
## Task 8: Скрипт deploy-to-staging и документация
**Files:**
- Создаём: `~/Documents/Claude/scripts/deploy-staging.sh`
- Создаём: `SBT/02-Стандарты/Процессы/lms-staging-deploy.md`
- [ ] **Шаг 8.1: Написать скрипт deploy-staging.sh**
```bash
cat > ~/Documents/Claude/scripts/deploy-staging.sh << 'SCRIPT'
#!/usr/bin/env bash
# Deploy LMS to staging (staging.school.second-brain.ru)
# Usage: bash deploy-staging.sh
set -euo pipefail
STAGING_SERVER="root@178.104.27.196"
BUILD_DIR="/root/lms-staging-build"
COMPOSE_DIR="/root/digital-household/lms-staging"
LOG_LINES=15
echo "=== LMS Staging Deploy ==="
echo "→ Pulling latest code..."
ssh "$STAGING_SERVER" "cd ${BUILD_DIR} && git pull 2>&1 | tail -5"
echo "→ Building image..."
ssh "$STAGING_SERVER" "cd ${BUILD_DIR} && docker build -t lms-staging-app:latest . 2>&1 | tail -5"
echo "→ Restarting app..."
ssh "$STAGING_SERVER" "cd ${COMPOSE_DIR} && docker compose up -d app"
echo "→ Waiting 10s for startup..."
sleep 10
echo "→ Logs:"
ssh "$STAGING_SERVER" "docker logs lms-staging-app-1 --tail ${LOG_LINES} 2>&1"
echo "→ Health check..."
CODE=$(curl -s -o /dev/null -w "%{http_code}" https://staging.school.second-brain.ru/login)
if [ "$CODE" = "200" ]; then
echo "✅ Staging OK — https://staging.school.second-brain.ru (HTTP $CODE)"
else
echo "❌ Staging returned HTTP $CODE"
exit 1
fi
SCRIPT
chmod +x ~/Documents/Claude/scripts/deploy-staging.sh
```
- [ ] **Шаг 8.2: Тестовый запуск скрипта**
```bash
bash ~/Documents/Claude/scripts/deploy-staging.sh
```
Ожидаем в конце: `✅ Staging OK — https://staging.school.second-brain.ru (HTTP 200)`
- [ ] **Шаг 8.3: Создать SBT-заметку**
Создать файл `SBT/02-Стандарты/Процессы/lms-staging-deploy.md` со следующим содержимым:
```markdown
---
type: process
title: "LMS — Деплой в staging"
status: active
created: 20260525
updated: 20260525
tags: [lms, staging, deploy, docker]
related: ["[[Процессы/lms-deploy]]", "[[Сервисы/hoster-kz]]", "[[Сервисы/vps-hetzner]]"]
---
# Процесс: Деплой LMS в staging
## Когда применяется
Перед выкаткой любых изменений в prod — сначала деплоим в staging и проверяем.
Также: тестирование миграций БД, проверка новых фич, отладка.
## Ключевые факты
| Что | Где |
|---|---|
| URL | https://staging.school.second-brain.ru |
| Сервер | Hetzner Antigravity-1 (178.104.27.196) |
| Compose | `/root/digital-household/lms-staging/` |
| Build dir | `/root/lms-staging-build/` |
| Порт | 3011 |
| БД | `lms_staging_db` (отдельный контейнер) |
| Email | Отключён (RESEND_API_KEY пустой) |
| S3 | Тот же бакет что и prod |
## Деплой одной командой (с Mac)
```bash
bash ~/Documents/Claude/scripts/deploy-staging.sh
```
## Ручные шаги (если нужно)
```bash
# На сервере
cd /root/lms-staging-build && git pull
docker build -t lms-staging-app:latest .
cd /root/digital-household/lms-staging && docker compose up -d app
docker logs lms-staging-app-1 --tail 20
```
## Откат
```bash
ssh root@178.104.27.196 "cd /root/digital-household/lms-staging && docker compose down"
```
## Восстановить prod-данные в staging (для отладки)
```bash
# 1. Взять свежий дамп с 3TB или Storage Box
# 2. Залить в staging-базу
DUMP="/Volumes/3TB/Second Brain Production/LMS Backups/lms-db-YYYYMMDD-HHMM.sql.gz"
scp "$DUMP" root@178.104.27.196:/tmp/staging-restore.sql.gz
ssh root@178.104.27.196 \
"zcat /tmp/staging-restore.sql.gz | docker exec -i lms-staging-db-1 psql -U lms_staging_user lms_staging_db"
```
## Связанные процессы
- [[Процессы/lms-deploy]] — деплой в prod (после проверки на staging)
- [[Процессы/lms-disaster-recovery]]
```
- [ ] **Шаг 8.4: Коммит скрипта**
```bash
cd ~/Documents/Claude/lms-system
git add docs/
git commit -m "Add LMS staging environment plan and deploy script"
git push origin HEAD
```
---
## Чек-лист финальной проверки
- [ ] `https://staging.school.second-brain.ru/login` → 200
- [ ] Войти под тестовым аккаунтом
- [ ] Курс открывается
- [ ] Email НЕ уходит при регистрации (проверить логи)
- [ ] `bash deploy-staging.sh` завершается с `✅ Staging OK`
- [ ] SBT-заметка создана
---
## Ожидаемое время
~30–40 минут (основное время — сборка Docker-образа ~3–5 мин).
+1
View File
@@ -0,0 +1 @@
{"0": "Admin Categories & UI Library", "1": "Auth Flow & App Settings", "2": "Markdown→TipTap Import", "3": "Email Notifications & Q&A API", "4": "Student Lesson Player", "5": "Auth/Session & Student Profile", "6": "Lesson Editor & Quiz Tools", "7": "Admin User Detail (Balance/Contact)", "8": "Next.js Config", "9": "Course Tree & Module Sorting", "10": "CSV Import/Export", "11": "Curator Homework Review", "12": "Components Manifest", "13": "Prisma Client & Dashboards", "14": "Admin Users List & Enroll", "15": "TSConfig", "16": "S3 Upload Routes", "17": "Module Page & Lesson Sorting", "18": "Cross-cutting Course Actions", "19": "Create-User Form", "20": "Load Testing", "21": "Admin Comments Moderation", "22": "Admin Questions Split View", "23": "Bridge: Shared Libs (auth/prisma/email)", "24": "Module Actions Standalone", "25": "Student Course Sidebar Layout", "26": "Question Thread UI", "27": "Admin/Curator Dashboards", "28": "Homework Filters", "29": "Student New Question Page", "30": "Prisma Seed", "31": "Admin Homework Actions", "32": "Middleware", "33": "Email→Homework→Questions Bridge", "34": "Tech Docs & Prisma Schema", "35": "Lesson Actions Lib", "36": "Student Comment Actions", "37": "Lesson Actions + Prisma Bridge", "38": "Project Docs (CLAUDE/ROADMAP)", "39": "Homework Editor↔Actions Pair", "40": "File-format Badge & Files Manager", "41": "Auth↔Middleware Pair", "42": "ESLint Config", "43": "PostCSS Config", "44": "Lesson Editor↔md-to-tiptap", "45": "Sortable Lessons↔Module Actions", "46": "Quiz Editor↔Quiz Actions", "47": "Backup Script", "48": "Verify Email Page", "49": "Maintenance Page", "50": "Sonner Toast", "51": "User Enrollment Pair", "52": "Community 52", "53": "Community 53", "54": "Community 54", "55": "Community 55", "56": "Community 56", "57": "Community 57", "58": "Community 58", "59": "Community 59", "60": "Community 60", "61": "Community 61", "62": "Community 62", "63": "Community 63", "64": "Community 64", "65": "Community 65", "66": "Community 66", "67": "Community 67", "68": "Community 68", "69": "Community 69", "70": "Community 70", "71": "Community 71", "72": "Community 72", "73": "Community 73", "74": "Community 74", "75": "Community 75", "76": "Community 76", "77": "Community 77", "78": "Community 78"}
+1
View File
@@ -0,0 +1 @@
/Users/dementiy/.local/share/uv/tools/graphifyy/bin/python3
+1
View File
@@ -0,0 +1 @@
/Users/dementiy/Documents/Claude/lms-system
+253
View File
@@ -0,0 +1,253 @@
# Graph Report - /Users/dementiy/Documents/Claude/lms-system (2026-05-20)
## Corpus Check
- 189 files · ~66,515 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 747 nodes · 1213 edges · 79 communities (37 shown, 42 thin omitted)
- Extraction: 100% EXTRACTED · 0% INFERRED · 0% AMBIGUOUS · INFERRED: 4 edges (avg confidence: 0.85)
- Token cost: 193,752 input · 7,975 output
## Community Hubs (Navigation)
- [[_COMMUNITY_Admin Categories & UI Library|Admin Categories & UI Library]]
- [[_COMMUNITY_Auth Flow & App Settings|Auth Flow & App Settings]]
- [[_COMMUNITY_Markdown→TipTap Import|Markdown→TipTap Import]]
- [[_COMMUNITY_Email Notifications & Q&A API|Email Notifications & Q&A API]]
- [[_COMMUNITY_Student Lesson Player|Student Lesson Player]]
- [[_COMMUNITY_AuthSession & Student Profile|Auth/Session & Student Profile]]
- [[_COMMUNITY_Lesson Editor & Quiz Tools|Lesson Editor & Quiz Tools]]
- [[_COMMUNITY_Admin User Detail (BalanceContact)|Admin User Detail (Balance/Contact)]]
- [[_COMMUNITY_Next.js Config|Next.js Config]]
- [[_COMMUNITY_Course Tree & Module Sorting|Course Tree & Module Sorting]]
- [[_COMMUNITY_CSV ImportExport|CSV Import/Export]]
- [[_COMMUNITY_Curator Homework Review|Curator Homework Review]]
- [[_COMMUNITY_Components Manifest|Components Manifest]]
- [[_COMMUNITY_Prisma Client & Dashboards|Prisma Client & Dashboards]]
- [[_COMMUNITY_Admin Users List & Enroll|Admin Users List & Enroll]]
- [[_COMMUNITY_TSConfig|TSConfig]]
- [[_COMMUNITY_S3 Upload Routes|S3 Upload Routes]]
- [[_COMMUNITY_Module Page & Lesson Sorting|Module Page & Lesson Sorting]]
- [[_COMMUNITY_Cross-cutting Course Actions|Cross-cutting Course Actions]]
- [[_COMMUNITY_Create-User Form|Create-User Form]]
- [[_COMMUNITY_Load Testing|Load Testing]]
- [[_COMMUNITY_Admin Comments Moderation|Admin Comments Moderation]]
- [[_COMMUNITY_Admin Questions Split View|Admin Questions Split View]]
- [[_COMMUNITY_Bridge Shared Libs (authprismaemail)|Bridge: Shared Libs (auth/prisma/email)]]
- [[_COMMUNITY_Module Actions Standalone|Module Actions Standalone]]
- [[_COMMUNITY_Student Course Sidebar Layout|Student Course Sidebar Layout]]
- [[_COMMUNITY_Question Thread UI|Question Thread UI]]
- [[_COMMUNITY_AdminCurator Dashboards|Admin/Curator Dashboards]]
- [[_COMMUNITY_Homework Filters|Homework Filters]]
- [[_COMMUNITY_Student New Question Page|Student New Question Page]]
- [[_COMMUNITY_Prisma Seed|Prisma Seed]]
- [[_COMMUNITY_Admin Homework Actions|Admin Homework Actions]]
- [[_COMMUNITY_Middleware|Middleware]]
- [[_COMMUNITY_Email→Homework→Questions Bridge|Email→Homework→Questions Bridge]]
- [[_COMMUNITY_Tech Docs & Prisma Schema|Tech Docs & Prisma Schema]]
- [[_COMMUNITY_Lesson Actions Lib|Lesson Actions Lib]]
- [[_COMMUNITY_Lesson Actions + Prisma Bridge|Lesson Actions + Prisma Bridge]]
- [[_COMMUNITY_Project Docs (CLAUDEROADMAP)|Project Docs (CLAUDE/ROADMAP)]]
- [[_COMMUNITY_Homework Editor↔Actions Pair|Homework Editor↔Actions Pair]]
- [[_COMMUNITY_File-format Badge & Files Manager|File-format Badge & Files Manager]]
- [[_COMMUNITY_Auth↔Middleware Pair|Auth↔Middleware Pair]]
- [[_COMMUNITY_ESLint Config|ESLint Config]]
- [[_COMMUNITY_PostCSS Config|PostCSS Config]]
- [[_COMMUNITY_Lesson Editor↔md-to-tiptap|Lesson Editor↔md-to-tiptap]]
- [[_COMMUNITY_Sortable Lessons↔Module Actions|Sortable Lessons↔Module Actions]]
- [[_COMMUNITY_Quiz Editor↔Quiz Actions|Quiz Editor↔Quiz Actions]]
- [[_COMMUNITY_User Enrollment Pair|User Enrollment Pair]]
- [[_COMMUNITY_Community 54|Community 54]]
- [[_COMMUNITY_Community 55|Community 55]]
- [[_COMMUNITY_Community 56|Community 56]]
- [[_COMMUNITY_Community 57|Community 57]]
- [[_COMMUNITY_Community 58|Community 58]]
- [[_COMMUNITY_Community 59|Community 59]]
- [[_COMMUNITY_Community 60|Community 60]]
- [[_COMMUNITY_Community 61|Community 61]]
- [[_COMMUNITY_Community 62|Community 62]]
- [[_COMMUNITY_Community 63|Community 63]]
- [[_COMMUNITY_Community 64|Community 64]]
- [[_COMMUNITY_Community 65|Community 65]]
- [[_COMMUNITY_Community 66|Community 66]]
- [[_COMMUNITY_Community 67|Community 67]]
- [[_COMMUNITY_Community 68|Community 68]]
- [[_COMMUNITY_Community 69|Community 69]]
- [[_COMMUNITY_Community 70|Community 70]]
- [[_COMMUNITY_Community 71|Community 71]]
- [[_COMMUNITY_Community 72|Community 72]]
- [[_COMMUNITY_Community 73|Community 73]]
- [[_COMMUNITY_Community 74|Community 74]]
- [[_COMMUNITY_Community 75|Community 75]]
- [[_COMMUNITY_Community 76|Community 76]]
- [[_COMMUNITY_Community 77|Community 77]]
- [[_COMMUNITY_Community 78|Community 78]]
## God Nodes (most connected - your core abstractions)
1. `auth` - 55 edges
2. `cn()` - 39 edges
3. `compilerOptions` - 16 edges
4. `getSetting()` - 15 edges
5. `uploadFile()` - 14 edges
6. `getSchoolName()` - 13 edges
7. `Prisma Client` - 13 edges
8. `getResend()` - 12 edges
9. `base()` - 12 edges
10. `btn()` - 12 edges
## Surprising Connections (you probably didn't know these)
- `cn()` --calls--> `clsx` [INFERRED]
src/lib/utils.ts → package.json
- `mdToTiptap()` --calls--> `unified` [INFERRED]
src/lib/md-to-tiptap.ts → package.json
- `Claude Project Rules` --references--> `Project Roadmap` [EXTRACTED]
CLAUDE.md → ROADMAP.md
- `Technical Documentation` --references--> `Prisma Schema` [EXTRACTED]
TECHNICAL.md → prisma/schema.prisma
- `generateMetadata()` --calls--> `getSettings` [EXTRACTED]
src/app/layout.tsx → src/lib/settings.ts
## Communities (79 total, 42 thin omitted)
### Community 0 - "Admin Categories & UI Library"
Cohesion: 0.06
Nodes (49): CategoryRow(), Props, Category, Course, CreateCourseDialog(), createCategory(), deleteCategory(), requireAdmin() (+41 more)
### Community 1 - "Auth Flow & App Settings"
Cohesion: 0.07
Nodes (29): AdminNav(), links, AdminShell(), focusHandlers, inputStyle, SettingsForm(), StopImpersonateBanner(), firaMono (+21 more)
### Community 2 - "Markdown→TipTap Import"
Cohesion: 0.05
Nodes (41): POST(), convertBlock(), convertInline(), Mark, MdastNode, mdToTiptap(), TipTapNode, dependencies (+33 more)
### Community 3 - "Email Notifications & Q&A API"
Cohesion: 0.15
Nodes (30): createModule(), deleteModule(), grantAccess(), reorderModules(), requireAdmin(), revokeAccess(), updateModule(), HomeworkFile (+22 more)
### Community 4 - "Student Lesson Player"
Cohesion: 0.08
Nodes (24): addComment(), deleteComment(), HomeworkFile, submitHomework(), submitQuizAttempt(), toggleLessonProgress(), parseNotificationEmails(), KinescopePlayer() (+16 more)
### Community 5 - "Auth/Session & Student Profile"
Cohesion: 0.09
Nodes (6): { GET, POST }, auth, Session, User, metadata, POST()
### Community 6 - "Lesson Editor & Quiz Tools"
Cohesion: 0.09
Nodes (22): deleteHomework(), requireAdmin(), saveHomework(), deleteQuiz(), requireAdmin(), saveQuiz(), HomeworkEditor(), Props (+14 more)
### Community 7 - "Admin User Detail (Balance/Contact)"
Cohesion: 0.10
Nodes (25): bulkGrantAccess(), requireAdmin(), revokeUserAccess(), focusHandlers, formatAmount(), inputStyle, Props, Transaction (+17 more)
### Community 8 - "Next.js Config"
Cohesion: 0.07
Nodes (26): nextConfig, devDependencies, dotenv, eslint, eslint-config-next, prisma, tailwindcss, @tailwindcss/postcss (+18 more)
### Community 9 - "Course Tree & Module Sorting"
Cohesion: 0.13
Nodes (19): createModule(), deleteModule(), grantAccess(), reorderModules(), requireAdmin(), revokeAccess(), updateModule(), CourseEditForm() (+11 more)
### Community 10 - "CSV Import/Export"
Cohesion: 0.12
Nodes (18): Course, CsvExporter(), focusHandlers, inputStyle, Course, CsvImporter(), focusHandlers, inputStyle (+10 more)
### Community 11 - "Curator Homework Review"
Cohesion: 0.12
Nodes (14): AudioRecorder(), AudioRecorderProps, ContentViewer(), asBool(), deleteSubmission(), requireCurator(), setReviewing(), submitFeedback() (+6 more)
### Community 12 - "Components Manifest"
Cohesion: 0.09
Nodes (21): aliases, components, hooks, lib, ui, utils, iconLibrary, menuAccent (+13 more)
### Community 13 - "Prisma Client & Dashboards"
Cohesion: 0.10
Nodes (7): Props, requireAdmin(), saveLesson(), globalForPrisma, Props, metadata, Props
### Community 14 - "Admin Users List & Enroll"
Cohesion: 0.12
Nodes (14): Course, Props, QuickEnrollButton(), inputStyle, UsersSearch(), Enrollment, roleLabel, roleVariant (+6 more)
### Community 15 - "TSConfig"
Cohesion: 0.10
Nodes (19): compilerOptions, allowJs, esModuleInterop, incremental, isolatedModules, jsx, lib, module (+11 more)
### Community 16 - "S3 Upload Routes"
Cohesion: 0.22
Nodes (12): POST(), POST(), DELETE(), PATCH(), POST(), requireAdmin(), deleteFile(), getPublicUrl() (+4 more)
### Community 17 - "Module Page & Lesson Sorting"
Cohesion: 0.23
Nodes (11): createLesson(), deleteLesson(), moveLessonToModule(), reorderLessons(), requireAdmin(), toggleLessonPublished(), updateLesson(), Lesson (+3 more)
### Community 18 - "Cross-cutting Course Actions"
Cohesion: 0.12
Nodes (16): Course Detail Page, Create Course Action, Admin Dashboard, AdminLayout(), Create Lesson Action, Create Module Action, User Detail Page, Create User Action (+8 more)
### Community 19 - "Create-User Form"
Cohesion: 0.18
Nodes (6): CreateUserForm(), focusHandlers, inputStyle, ROLES, metadata, createUser()
### Community 20 - "Load Testing"
Cohesion: 0.20
Nodes (9): BROWSER_HEADERS, courseRes, dashRes, jar, lessonRes, LESSONS, loginRes, options (+1 more)
### Community 21 - "Admin Comments Moderation"
Cohesion: 0.24
Nodes (6): Comment, CommentsTable(), inputStyle, adminDeleteComment(), metadata, Props
### Community 22 - "Admin Questions Split View"
Cohesion: 0.22
Nodes (5): FileAttachment, Message, QuestionDetail, QuestionSplitView(), QuestionSummary
### Community 23 - "Bridge: Shared Libs (auth/prisma/email)"
Cohesion: 0.22
Nodes (9): AudioRecorder, Better Auth, grantAccess, Email Senders, EnrollmentManager, HomeworkSection, Prisma Client, getSettings (+1 more)
### Community 24 - "Module Actions Standalone"
Cohesion: 0.46
Nodes (7): createLesson(), deleteLesson(), moveLessonToModule(), reorderLessons(), requireAdmin(), toggleLessonPublished(), updateLesson()
### Community 25 - "Student Course Sidebar Layout"
Cohesion: 0.29
Nodes (5): Props, Course, CourseSidebar(), Lesson, Module
### Community 26 - "Question Thread UI"
Cohesion: 0.29
Nodes (4): FileAttachment, Message, QuestionThread(), QuestionThreadProps
### Community 31 - "Admin Homework Actions"
Cohesion: 0.83
Nodes (3): deleteHomework(), requireAdmin(), saveHomework()
### Community 33 - "Email→Homework→Questions Bridge"
Cohesion: 0.67
Nodes (3): Questions API Route, Email Helpers (Resend), Homework Submission Actions
### Community 37 - "Lesson Actions + Prisma Bridge"
Cohesion: 0.67
Nodes (3): Prisma Client Singleton, Lesson Comment Actions, Lesson Progress Actions
## Knowledge Gaps
- **278 isolated node(s):** `config`, `LESSONS`, `TEST_USER`, `BROWSER_HEADERS`, `options` (+273 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **42 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `dependencies` connect `Markdown→TipTap Import` to `Next.js Config`, `Cross-cutting Course Actions`?**
_High betweenness centrality (0.128) - this node is a cross-community bridge._
- **Why does `auth` connect `Auth/Session & Student Profile` to `Admin Categories & UI Library`, `Auth Flow & App Settings`, `Markdown→TipTap Import`, `Email Notifications & Q&A API`, `Student Lesson Player`, `Lesson Editor & Quiz Tools`, `Admin User Detail (Balance/Contact)`, `Course Tree & Module Sorting`, `CSV Import/Export`, `Curator Homework Review`, `Prisma Client & Dashboards`, `Admin Users List & Enroll`, `S3 Upload Routes`, `Module Page & Lesson Sorting`, `Admin Comments Moderation`, `Admin Questions Split View`, `Module Actions Standalone`, `Student Course Sidebar Layout`, `Admin/Curator Dashboards`, `Admin Homework Actions`, `Lesson Actions Lib`, `Student Comment Actions`?**
_High betweenness centrality (0.121) - this node is a cross-community bridge._
- **What connects `config`, `LESSONS`, `TEST_USER` to the rest of the system?**
_278 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `Admin Categories & UI Library` be split into smaller, more focused modules?**
_Cohesion score 0.06377151799687011 - nodes in this community are weakly interconnected._
- **Should `Auth Flow & App Settings` be split into smaller, more focused modules?**
_Cohesion score 0.06787330316742081 - nodes in this community are weakly interconnected._
- **Should `Markdown→TipTap Import` be split into smaller, more focused modules?**
_Cohesion score 0.05094130675526024 - nodes in this community are weakly interconnected._
- **Should `Student Lesson Player` be split into smaller, more focused modules?**
_Cohesion score 0.08235294117647059 - nodes in this community are weakly interconnected._
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_components_layout_logout_button_tsx", "label": "logout-button.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/layout/logout-button.tsx", "source_location": "L1"}, {"id": "layout_logout_button_logoutbutton", "label": "LogoutButton()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/layout/logout-button.tsx", "source_location": "L6"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_components_layout_logout_button_tsx", "target": "navigation", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/layout/logout-button.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_layout_logout_button_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_auth_client_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/layout/logout-button.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_layout_logout_button_tsx", "target": "lib_auth_client_signout", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/layout/logout-button.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_layout_logout_button_tsx", "target": "layout_logout_button_logoutbutton", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/layout/logout-button.tsx", "source_location": "L6", "weight": 1.0}], "raw_calls": [{"caller_nid": "layout_logout_button_logoutbutton", "callee": "useRouter", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/layout/logout-button.tsx", "source_location": "L7"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_prisma_config_ts", "label": "prisma.config.ts", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/prisma.config.ts", "source_location": "L1"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_prisma_config_ts", "target": "config", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/prisma.config.ts", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_prisma_config_ts", "target": "config", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/prisma.config.ts", "source_location": "L4", "weight": 1.0}], "raw_calls": []}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_admin_import_export_page_tsx", "label": "page.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/import-export/page.tsx", "source_location": "L1"}, {"id": "import_export_page_metadata", "label": "metadata", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/import-export/page.tsx", "source_location": "L5"}, {"id": "import_export_page_importexportpage", "label": "ImportExportPage()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/import-export/page.tsx", "source_location": "L7"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_admin_import_export_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_prisma_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/import-export/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_import_export_page_tsx", "target": "lib_prisma_prisma", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/import-export/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_import_export_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_components_admin_csv_importer_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/import-export/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_import_export_page_tsx", "target": "admin_csv_importer_csvimporter", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/import-export/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_import_export_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_components_admin_csv_exporter_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/import-export/page.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_import_export_page_tsx", "target": "admin_csv_exporter_csvexporter", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/import-export/page.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_import_export_page_tsx", "target": "import_export_page_metadata", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/import-export/page.tsx", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_import_export_page_tsx", "target": "import_export_page_importexportpage", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/import-export/page.tsx", "source_location": "L7", "weight": 1.0}], "raw_calls": [{"caller_nid": "import_export_page_importexportpage", "callee": "findMany", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/import-export/page.tsx", "source_location": "L8"}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_admin_comments_actions_ts", "label": "actions.ts", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/actions.ts", "source_location": "L1"}, {"id": "comments_actions_admindeletecomment", "label": "adminDeleteComment()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/actions.ts", "source_location": "L8"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_admin_comments_actions_ts", "target": "headers", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/actions.ts", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_comments_actions_ts", "target": "users_dementiy_documents_claude_lms_system_src_lib_auth_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/actions.ts", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_comments_actions_ts", "target": "lib_auth_auth", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/actions.ts", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_comments_actions_ts", "target": "users_dementiy_documents_claude_lms_system_src_lib_prisma_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/actions.ts", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_comments_actions_ts", "target": "lib_prisma_prisma", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/actions.ts", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_comments_actions_ts", "target": "cache", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/actions.ts", "source_location": "L6", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_comments_actions_ts", "target": "comments_actions_admindeletecomment", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/actions.ts", "source_location": "L8", "weight": 1.0}], "raw_calls": [{"caller_nid": "comments_actions_admindeletecomment", "callee": "getSession", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/actions.ts", "source_location": "L9"}, {"caller_nid": "comments_actions_admindeletecomment", "callee": "headers", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/actions.ts", "source_location": "L9"}, {"caller_nid": "comments_actions_admindeletecomment", "callee": "update", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/actions.ts", "source_location": "L12"}, {"caller_nid": "comments_actions_admindeletecomment", "callee": "revalidatePath", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/actions.ts", "source_location": "L17"}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_components_admin_homework_filters_tsx", "label": "homework-filters.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L1"}, {"id": "admin_homework_filters_inputstyle", "label": "inputStyle", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L7"}, {"id": "admin_homework_filters_course", "label": "Course", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L16"}, {"id": "admin_homework_filters_homeworkfilters", "label": "HomeworkFilters()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L18"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_components_admin_homework_filters_tsx", "target": "navigation", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_homework_filters_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_homework_filters_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_homework_filters_tsx", "target": "admin_homework_filters_inputstyle", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L7", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_homework_filters_tsx", "target": "admin_homework_filters_course", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L16", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_homework_filters_tsx", "target": "admin_homework_filters_homeworkfilters", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L18", "weight": 1.0}], "raw_calls": [{"caller_nid": "admin_homework_filters_homeworkfilters", "callee": "useRouter", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L19"}, {"caller_nid": "admin_homework_filters_homeworkfilters", "callee": "usePathname", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L20"}, {"caller_nid": "admin_homework_filters_homeworkfilters", "callee": "useSearchParams", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L21"}, {"caller_nid": "admin_homework_filters_homeworkfilters", "callee": "useTransition", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L22"}, {"caller_nid": "admin_homework_filters_homeworkfilters", "callee": "get", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L32"}, {"caller_nid": "admin_homework_filters_homeworkfilters", "callee": "get", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L33"}, {"caller_nid": "admin_homework_filters_homeworkfilters", "callee": "get", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L34"}, {"caller_nid": "admin_homework_filters_homeworkfilters", "callee": "map", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/homework-filters.tsx", "source_location": "L82"}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_postcss_config_mjs", "label": "postcss.config.mjs", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/postcss.config.mjs", "source_location": "L1"}, {"id": "lms_system_postcss_config_config", "label": "config", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/postcss.config.mjs", "source_location": "L1"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_postcss_config_mjs", "target": "lms_system_postcss_config_config", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/postcss.config.mjs", "source_location": "L1", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_auth_register_register_form_tsx", "label": "register-form.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L1"}, {"id": "register_register_form_props", "label": "Props", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L7"}, {"id": "register_register_form_registerform", "label": "RegisterForm()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L14"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_auth_register_register_form_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_register_register_form_tsx", "target": "link", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_register_register_form_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_auth_client_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_register_register_form_tsx", "target": "lib_auth_client_signup", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_register_register_form_tsx", "target": "register_register_form_props", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L7", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_register_register_form_tsx", "target": "register_register_form_registerform", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L14", "weight": 1.0}], "raw_calls": [{"caller_nid": "register_register_form_registerform", "callee": "useState", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L15"}, {"caller_nid": "register_register_form_registerform", "callee": "useState", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L16"}, {"caller_nid": "register_register_form_registerform", "callee": "useState", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L17"}, {"caller_nid": "register_register_form_registerform", "callee": "useState", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L18"}, {"caller_nid": "register_register_form_registerform", "callee": "useState", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L19"}, {"caller_nid": "register_register_form_registerform", "callee": "useState", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L20"}, {"caller_nid": "register_register_form_registerform", "callee": "useState", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L21"}, {"caller_nid": "register_register_form_registerform", "callee": "filter", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L33"}, {"caller_nid": "register_register_form_registerform", "callee": "map", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/register-form.tsx", "source_location": "L136"}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_lib_prisma_ts", "label": "prisma.ts", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/prisma.ts", "source_location": "L1"}, {"id": "lib_prisma_globalforprisma", "label": "globalForPrisma", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/prisma.ts", "source_location": "L4"}, {"id": "lib_prisma_createprismaclient", "label": "createPrismaClient()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/prisma.ts", "source_location": "L8"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_lib_prisma_ts", "target": "users_dementiy_documents_claude_lms_system_src_generated_prisma_client_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/prisma.ts", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_lib_prisma_ts", "target": "prisma_client_prismaclient", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/prisma.ts", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_lib_prisma_ts", "target": "adapter_pg", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/prisma.ts", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_lib_prisma_ts", "target": "lib_prisma_globalforprisma", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/prisma.ts", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_lib_prisma_ts", "target": "lib_prisma_createprismaclient", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/prisma.ts", "source_location": "L8", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_student_profile_actions_ts", "label": "actions.ts", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L1"}, {"id": "profile_actions_changepasswordaction", "label": "changePasswordAction()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L8"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_student_profile_actions_ts", "target": "headers", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_student_profile_actions_ts", "target": "users_dementiy_documents_claude_lms_system_src_lib_auth_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_student_profile_actions_ts", "target": "lib_auth_auth", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_student_profile_actions_ts", "target": "users_dementiy_documents_claude_lms_system_src_lib_prisma_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_student_profile_actions_ts", "target": "lib_prisma_prisma", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_student_profile_actions_ts", "target": "bcryptjs", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L6", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_student_profile_actions_ts", "target": "profile_actions_changepasswordaction", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L8", "weight": 1.0}], "raw_calls": [{"caller_nid": "profile_actions_changepasswordaction", "callee": "get", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L9"}, {"caller_nid": "profile_actions_changepasswordaction", "callee": "get", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L10"}, {"caller_nid": "profile_actions_changepasswordaction", "callee": "get", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L11"}, {"caller_nid": "profile_actions_changepasswordaction", "callee": "getSession", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L17"}, {"caller_nid": "profile_actions_changepasswordaction", "callee": "headers", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L17"}, {"caller_nid": "profile_actions_changepasswordaction", "callee": "findFirst", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L20"}, {"caller_nid": "profile_actions_changepasswordaction", "callee": "compare", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L25"}, {"caller_nid": "profile_actions_changepasswordaction", "callee": "hash", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L28"}, {"caller_nid": "profile_actions_changepasswordaction", "callee": "update", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/profile/actions.ts", "source_location": "L29"}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_components_ui_textarea_tsx", "label": "textarea.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/textarea.tsx", "source_location": "L1"}, {"id": "ui_textarea_textarea", "label": "Textarea()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/textarea.tsx", "source_location": "L5"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_components_ui_textarea_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/textarea.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_textarea_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/textarea.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_textarea_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/textarea.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_textarea_tsx", "target": "ui_textarea_textarea", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/textarea.tsx", "source_location": "L5", "weight": 1.0}], "raw_calls": [{"caller_nid": "ui_textarea_textarea", "callee": "cn", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/textarea.tsx", "source_location": "L9"}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_admin_quizzes_quizid_page_tsx", "label": "page.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/[quizId]/page.tsx", "source_location": "L1"}, {"id": "quizid_page_props", "label": "Props", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/[quizId]/page.tsx", "source_location": "L5"}, {"id": "quizid_page_adminquizattemptspage", "label": "AdminQuizAttemptsPage()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/[quizId]/page.tsx", "source_location": "L9"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_admin_quizzes_quizid_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_prisma_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/[quizId]/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_quizzes_quizid_page_tsx", "target": "lib_prisma_prisma", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/[quizId]/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_quizzes_quizid_page_tsx", "target": "navigation", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/[quizId]/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_quizzes_quizid_page_tsx", "target": "link", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/[quizId]/page.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_quizzes_quizid_page_tsx", "target": "quizid_page_props", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/[quizId]/page.tsx", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_quizzes_quizid_page_tsx", "target": "quizid_page_adminquizattemptspage", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/[quizId]/page.tsx", "source_location": "L9", "weight": 1.0}], "raw_calls": [{"caller_nid": "quizid_page_adminquizattemptspage", "callee": "findUnique", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/[quizId]/page.tsx", "source_location": "L12"}, {"caller_nid": "quizid_page_adminquizattemptspage", "callee": "notFound", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/[quizId]/page.tsx", "source_location": "L30"}, {"caller_nid": "quizid_page_adminquizattemptspage", "callee": "map", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/[quizId]/page.tsx", "source_location": "L32"}, {"caller_nid": "quizid_page_adminquizattemptspage", "callee": "findMany", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/[quizId]/page.tsx", "source_location": "L33"}, {"caller_nid": "quizid_page_adminquizattemptspage", "callee": "fromEntries", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/[quizId]/page.tsx", "source_location": "L37"}, {"caller_nid": "quizid_page_adminquizattemptspage", "callee": "map", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/[quizId]/page.tsx", "source_location": "L37"}, {"caller_nid": "quizid_page_adminquizattemptspage", "callee": "map", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/[quizId]/page.tsx", "source_location": "L65"}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_admin_settings_page_tsx", "label": "page.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/settings/page.tsx", "source_location": "L1"}, {"id": "settings_page_metadata", "label": "metadata", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/settings/page.tsx", "source_location": "L4"}, {"id": "settings_page_settingspage", "label": "SettingsPage()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/settings/page.tsx", "source_location": "L6"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_admin_settings_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_settings_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/settings/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_settings_page_tsx", "target": "lib_settings_getsettings", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/settings/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_settings_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_components_admin_settings_form_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/settings/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_settings_page_tsx", "target": "admin_settings_form_settingsform", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/settings/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_settings_page_tsx", "target": "settings_page_metadata", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/settings/page.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_settings_page_tsx", "target": "settings_page_settingspage", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/settings/page.tsx", "source_location": "L6", "weight": 1.0}], "raw_calls": [{"caller_nid": "settings_page_settingspage", "callee": "getSettings", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/settings/page.tsx", "source_location": "L7"}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_components_ui_badge_tsx", "label": "badge.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/badge.tsx", "source_location": "L1"}, {"id": "ui_badge_badgevariants", "label": "badgeVariants", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/badge.tsx", "source_location": "L7"}, {"id": "ui_badge_badge", "label": "Badge()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/badge.tsx", "source_location": "L30"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_components_ui_badge_tsx", "target": "merge_props", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/badge.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_badge_tsx", "target": "use_render", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/badge.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_badge_tsx", "target": "class_variance_authority", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/badge.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_badge_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/badge.tsx", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_badge_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/badge.tsx", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_badge_tsx", "target": "ui_badge_badgevariants", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/badge.tsx", "source_location": "L7", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_badge_tsx", "target": "ui_badge_badge", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/badge.tsx", "source_location": "L30", "weight": 1.0}, {"source": "ui_badge_badge", "target": "ui_badge_badgevariants", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/badge.tsx", "source_location": "L40", "weight": 1.0}], "raw_calls": [{"caller_nid": "ui_badge_badge", "callee": "useRender", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/badge.tsx", "source_location": "L36"}, {"caller_nid": "ui_badge_badge", "callee": "mergeProps", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/badge.tsx", "source_location": "L38"}, {"caller_nid": "ui_badge_badge", "callee": "cn", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/badge.tsx", "source_location": "L40"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_auth_login_page_tsx", "label": "page.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/login/page.tsx", "source_location": "L1"}, {"id": "login_page_loginpage", "label": "LoginPage()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/login/page.tsx", "source_location": "L4"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_auth_login_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_settings_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/login/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_login_page_tsx", "target": "lib_settings_getsetting", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/login/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_login_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_app_auth_login_login_form_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/login/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_login_page_tsx", "target": "login_login_form_loginform", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/login/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_login_page_tsx", "target": "login_page_loginpage", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/login/page.tsx", "source_location": "L4", "weight": 1.0}], "raw_calls": [{"caller_nid": "login_page_loginpage", "callee": "all", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/login/page.tsx", "source_location": "L9"}, {"caller_nid": "login_page_loginpage", "callee": "getSetting", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/login/page.tsx", "source_location": "L10"}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_components_admin_admin_nav_tsx", "label": "admin-nav.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/admin-nav.tsx", "source_location": "L1"}, {"id": "admin_admin_nav_links", "label": "links", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/admin-nav.tsx", "source_location": "L6"}, {"id": "admin_admin_nav_adminnav", "label": "AdminNav()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/admin-nav.tsx", "source_location": "L19"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_components_admin_admin_nav_tsx", "target": "link", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/admin-nav.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_admin_nav_tsx", "target": "navigation", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/admin-nav.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_admin_nav_tsx", "target": "admin_admin_nav_links", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/admin-nav.tsx", "source_location": "L6", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_admin_nav_tsx", "target": "admin_admin_nav_adminnav", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/admin-nav.tsx", "source_location": "L19", "weight": 1.0}], "raw_calls": [{"caller_nid": "admin_admin_nav_adminnav", "callee": "usePathname", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/admin-nav.tsx", "source_location": "L20"}, {"caller_nid": "admin_admin_nav_adminnav", "callee": "map", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/admin-nav.tsx", "source_location": "L24"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_auth_register_page_tsx", "label": "page.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/page.tsx", "source_location": "L1"}, {"id": "register_page_registerpage", "label": "RegisterPage()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/page.tsx", "source_location": "L5"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_auth_register_page_tsx", "target": "navigation", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_register_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_settings_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_register_page_tsx", "target": "lib_settings_getsettings", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_register_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_app_auth_register_register_form_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/page.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_register_page_tsx", "target": "register_register_form_registerform", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/page.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_register_page_tsx", "target": "register_page_registerpage", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/page.tsx", "source_location": "L5", "weight": 1.0}], "raw_calls": [{"caller_nid": "register_page_registerpage", "callee": "getSettings", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/page.tsx", "source_location": "L6"}, {"caller_nid": "register_page_registerpage", "callee": "redirect", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/register/page.tsx", "source_location": "L9"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_components_ui_label_tsx", "label": "label.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/label.tsx", "source_location": "L1"}, {"id": "ui_label_label", "label": "Label()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/label.tsx", "source_location": "L7"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_components_ui_label_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/label.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_label_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/label.tsx", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_label_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/label.tsx", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_label_tsx", "target": "ui_label_label", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/label.tsx", "source_location": "L7", "weight": 1.0}], "raw_calls": [{"caller_nid": "ui_label_label", "callee": "cn", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/label.tsx", "source_location": "L11"}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_lib_s3_ts", "label": "s3.ts", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/s3.ts", "source_location": "L1"}, {"id": "lib_s3_s3", "label": "s3", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/s3.ts", "source_location": "L4"}, {"id": "lib_s3_getpublicurl", "label": "getPublicUrl()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/s3.ts", "source_location": "L16"}, {"id": "lib_s3_uploadfile", "label": "uploadFile()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/s3.ts", "source_location": "L20"}, {"id": "lib_s3_deletefile", "label": "deleteFile()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/s3.ts", "source_location": "L36"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_lib_s3_ts", "target": "client_s3", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/s3.ts", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_lib_s3_ts", "target": "s3_request_presigner", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/s3.ts", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_lib_s3_ts", "target": "lib_s3_s3", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/s3.ts", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_lib_s3_ts", "target": "lib_s3_getpublicurl", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/s3.ts", "source_location": "L16", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_lib_s3_ts", "target": "lib_s3_uploadfile", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/s3.ts", "source_location": "L20", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_lib_s3_ts", "target": "lib_s3_deletefile", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/s3.ts", "source_location": "L36", "weight": 1.0}, {"source": "lib_s3_uploadfile", "target": "lib_s3_getpublicurl", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/s3.ts", "source_location": "L33", "weight": 1.0}], "raw_calls": [{"caller_nid": "lib_s3_uploadfile", "callee": "send", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/s3.ts", "source_location": "L25"}, {"caller_nid": "lib_s3_deletefile", "callee": "send", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/lib/s3.ts", "source_location": "L37"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_components_admin_admin_shell_tsx", "label": "admin-shell.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/admin-shell.tsx", "source_location": "L1"}, {"id": "admin_admin_shell_adminshell", "label": "AdminShell()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/admin-shell.tsx", "source_location": "L4"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_components_admin_admin_shell_tsx", "target": "users_dementiy_documents_claude_lms_system_src_components_admin_admin_nav_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/admin-shell.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_admin_shell_tsx", "target": "admin_admin_nav_adminnav", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/admin-shell.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_admin_shell_tsx", "target": "users_dementiy_documents_claude_lms_system_src_components_layout_logout_button_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/admin-shell.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_admin_shell_tsx", "target": "layout_logout_button_logoutbutton", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/admin-shell.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_admin_shell_tsx", "target": "admin_admin_shell_adminshell", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/admin-shell.tsx", "source_location": "L4", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_eslint_config_mjs", "label": "eslint.config.mjs", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/eslint.config.mjs", "source_location": "L1"}, {"id": "lms_system_eslint_config_eslintconfig", "label": "eslintConfig", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/eslint.config.mjs", "source_location": "L5"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_eslint_config_mjs", "target": "config", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/eslint.config.mjs", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_eslint_config_mjs", "target": "core_web_vitals", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/eslint.config.mjs", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_eslint_config_mjs", "target": "typescript", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/eslint.config.mjs", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_eslint_config_mjs", "target": "lms_system_eslint_config_eslintconfig", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/eslint.config.mjs", "source_location": "L5", "weight": 1.0}], "raw_calls": []}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_admin_comments_delete_action_ts", "label": "delete-action.ts", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/delete-action.ts", "source_location": "L1"}, {"id": "comments_delete_action_deletecomment", "label": "deleteComment()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/delete-action.ts", "source_location": "L8"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_admin_comments_delete_action_ts", "target": "headers", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/delete-action.ts", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_comments_delete_action_ts", "target": "users_dementiy_documents_claude_lms_system_src_lib_auth_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/delete-action.ts", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_comments_delete_action_ts", "target": "lib_auth_auth", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/delete-action.ts", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_comments_delete_action_ts", "target": "users_dementiy_documents_claude_lms_system_src_lib_prisma_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/delete-action.ts", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_comments_delete_action_ts", "target": "lib_prisma_prisma", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/delete-action.ts", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_comments_delete_action_ts", "target": "cache", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/delete-action.ts", "source_location": "L6", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_comments_delete_action_ts", "target": "comments_delete_action_deletecomment", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/delete-action.ts", "source_location": "L8", "weight": 1.0}], "raw_calls": [{"caller_nid": "comments_delete_action_deletecomment", "callee": "getSession", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/delete-action.ts", "source_location": "L9"}, {"caller_nid": "comments_delete_action_deletecomment", "callee": "headers", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/delete-action.ts", "source_location": "L9"}, {"caller_nid": "comments_delete_action_deletecomment", "callee": "update", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/delete-action.ts", "source_location": "L12"}, {"caller_nid": "comments_delete_action_deletecomment", "callee": "revalidatePath", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/comments/delete-action.ts", "source_location": "L17"}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_components_ui_button_tsx", "label": "button.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/button.tsx", "source_location": "L1"}, {"id": "ui_button_buttonvariants", "label": "buttonVariants", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/button.tsx", "source_location": "L6"}, {"id": "ui_button_button", "label": "Button()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/button.tsx", "source_location": "L43"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_components_ui_button_tsx", "target": "button", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/button.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_button_tsx", "target": "class_variance_authority", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/button.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_button_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/button.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_button_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/button.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_button_tsx", "target": "ui_button_buttonvariants", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/button.tsx", "source_location": "L6", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_button_tsx", "target": "ui_button_button", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/button.tsx", "source_location": "L43", "weight": 1.0}, {"source": "ui_button_button", "target": "ui_button_buttonvariants", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/button.tsx", "source_location": "L52", "weight": 1.0}], "raw_calls": [{"caller_nid": "ui_button_button", "callee": "cn", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/button.tsx", "source_location": "L52"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_entrypoint_sh", "label": "entrypoint.sh", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/entrypoint.sh", "source_location": "L1"}], "edges": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_auth_forgot_password_page_tsx", "label": "page.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/forgot-password/page.tsx", "source_location": "L1"}, {"id": "forgot_password_page_forgotpasswordpage", "label": "ForgotPasswordPage()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/forgot-password/page.tsx", "source_location": "L4"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_auth_forgot_password_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_settings_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/forgot-password/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_forgot_password_page_tsx", "target": "lib_settings_getsetting", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/forgot-password/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_forgot_password_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_app_auth_forgot_password_forgot_password_form_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/forgot-password/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_forgot_password_page_tsx", "target": "forgot_password_forgot_password_form_forgotpasswordform", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/forgot-password/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_forgot_password_page_tsx", "target": "forgot_password_page_forgotpasswordpage", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/forgot-password/page.tsx", "source_location": "L4", "weight": 1.0}], "raw_calls": [{"caller_nid": "forgot_password_page_forgotpasswordpage", "callee": "getSetting", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/forgot-password/page.tsx", "source_location": "L5"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_components_ui_input_tsx", "label": "input.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/input.tsx", "source_location": "L1"}, {"id": "ui_input_input", "label": "Input()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/input.tsx", "source_location": "L6"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_components_ui_input_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/input.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_input_tsx", "target": "input", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/input.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_input_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_utils_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/input.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_input_tsx", "target": "lib_utils_cn", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/input.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_ui_input_tsx", "target": "ui_input_input", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/input.tsx", "source_location": "L6", "weight": 1.0}], "raw_calls": [{"caller_nid": "ui_input_input", "callee": "cn", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/ui/input.tsx", "source_location": "L11"}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_admin_questions_page_tsx", "label": "page.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/questions/page.tsx", "source_location": "L1"}, {"id": "questions_page_adminquestionspage", "label": "AdminQuestionsPage()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/questions/page.tsx", "source_location": "L6"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_admin_questions_page_tsx", "target": "headers", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/questions/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_questions_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_auth_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/questions/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_questions_page_tsx", "target": "lib_auth_auth", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/questions/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_questions_page_tsx", "target": "navigation", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/questions/page.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_questions_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_components_questions_questionsplitview_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/questions/page.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_questions_page_tsx", "target": "questions_questionsplitview_questionsplitview", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/questions/page.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_questions_page_tsx", "target": "questions_page_adminquestionspage", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/questions/page.tsx", "source_location": "L6", "weight": 1.0}], "raw_calls": [{"caller_nid": "questions_page_adminquestionspage", "callee": "getSession", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/questions/page.tsx", "source_location": "L7"}, {"caller_nid": "questions_page_adminquestionspage", "callee": "headers", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/questions/page.tsx", "source_location": "L7"}, {"caller_nid": "questions_page_adminquestionspage", "callee": "redirect", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/questions/page.tsx", "source_location": "L8"}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_auth_verify_email_page_tsx", "label": "page.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/verify-email/page.tsx", "source_location": "L1"}, {"id": "verify_email_page_verifyemailpage", "label": "VerifyEmailPage()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/verify-email/page.tsx", "source_location": "L3"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_auth_verify_email_page_tsx", "target": "link", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/verify-email/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_auth_verify_email_page_tsx", "target": "verify_email_page_verifyemailpage", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(auth)/verify-email/page.tsx", "source_location": "L3", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_admin_layout_tsx", "label": "layout.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/layout.tsx", "source_location": "L1"}, {"id": "admin_layout_adminlayout", "label": "AdminLayout()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/layout.tsx", "source_location": "L7"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_admin_layout_tsx", "target": "headers", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/layout.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_layout_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_auth_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/layout.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_layout_tsx", "target": "lib_auth_auth", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/layout.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_layout_tsx", "target": "navigation", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/layout.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_layout_tsx", "target": "users_dementiy_documents_claude_lms_system_src_components_admin_admin_shell_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/layout.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_layout_tsx", "target": "admin_admin_shell_adminshell", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/layout.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_layout_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_prisma_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/layout.tsx", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_layout_tsx", "target": "lib_prisma_prisma", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/layout.tsx", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_layout_tsx", "target": "admin_layout_adminlayout", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/layout.tsx", "source_location": "L7", "weight": 1.0}], "raw_calls": [{"caller_nid": "admin_layout_adminlayout", "callee": "getSession", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/layout.tsx", "source_location": "L8"}, {"caller_nid": "admin_layout_adminlayout", "callee": "headers", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/layout.tsx", "source_location": "L8"}, {"caller_nid": "admin_layout_adminlayout", "callee": "redirect", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/layout.tsx", "source_location": "L9"}, {"caller_nid": "admin_layout_adminlayout", "callee": "redirect", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/layout.tsx", "source_location": "L10"}, {"caller_nid": "admin_layout_adminlayout", "callee": "count", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/layout.tsx", "source_location": "L12"}]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_admin_users_new_page_tsx", "label": "page.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/users/new/page.tsx", "source_location": "L1"}, {"id": "new_page_metadata", "label": "metadata", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/users/new/page.tsx", "source_location": "L4"}, {"id": "new_page_newuserpage", "label": "NewUserPage()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/users/new/page.tsx", "source_location": "L6"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_admin_users_new_page_tsx", "target": "link", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/users/new/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_users_new_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_components_admin_create_user_form_tsx", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/users/new/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_users_new_page_tsx", "target": "admin_create_user_form_createuserform", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/users/new/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_users_new_page_tsx", "target": "new_page_metadata", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/users/new/page.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_users_new_page_tsx", "target": "new_page_newuserpage", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/users/new/page.tsx", "source_location": "L6", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_api_questions_id_close_route_ts", "label": "route.ts", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L1"}, {"id": "close_route_patch", "label": "PATCH()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L6"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_api_questions_id_close_route_ts", "target": "server", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_api_questions_id_close_route_ts", "target": "headers", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_api_questions_id_close_route_ts", "target": "users_dementiy_documents_claude_lms_system_src_lib_auth_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_api_questions_id_close_route_ts", "target": "lib_auth_auth", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_api_questions_id_close_route_ts", "target": "users_dementiy_documents_claude_lms_system_src_lib_prisma_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_api_questions_id_close_route_ts", "target": "lib_prisma_prisma", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_api_questions_id_close_route_ts", "target": "close_route_patch", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L6", "weight": 1.0}], "raw_calls": [{"caller_nid": "close_route_patch", "callee": "getSession", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L10"}, {"caller_nid": "close_route_patch", "callee": "headers", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L10"}, {"caller_nid": "close_route_patch", "callee": "json", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L11"}, {"caller_nid": "close_route_patch", "callee": "json", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L14"}, {"caller_nid": "close_route_patch", "callee": "findUnique", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L19"}, {"caller_nid": "close_route_patch", "callee": "json", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L20"}, {"caller_nid": "close_route_patch", "callee": "json", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L22"}, {"caller_nid": "close_route_patch", "callee": "update", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L25"}, {"caller_nid": "close_route_patch", "callee": "json", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/api/questions/[id]/close/route.ts", "source_location": "L30"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_admin_quizzes_page_tsx", "label": "page.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/page.tsx", "source_location": "L1"}, {"id": "quizzes_page_metadata", "label": "metadata", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/page.tsx", "source_location": "L4"}, {"id": "quizzes_page_adminquizzespage", "label": "AdminQuizzesPage()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/page.tsx", "source_location": "L6"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_admin_quizzes_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_prisma_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_quizzes_page_tsx", "target": "lib_prisma_prisma", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_quizzes_page_tsx", "target": "link", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_quizzes_page_tsx", "target": "quizzes_page_metadata", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/page.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_admin_quizzes_page_tsx", "target": "quizzes_page_adminquizzespage", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/page.tsx", "source_location": "L6", "weight": 1.0}], "raw_calls": [{"caller_nid": "quizzes_page_adminquizzespage", "callee": "findMany", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/page.tsx", "source_location": "L7"}, {"caller_nid": "quizzes_page_adminquizzespage", "callee": "map", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/admin/quizzes/page.tsx", "source_location": "L44"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_app_student_courses_slug_page_tsx", "label": "page.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/courses/[slug]/page.tsx", "source_location": "L1"}, {"id": "slug_page_props", "label": "Props", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/courses/[slug]/page.tsx", "source_location": "L5"}, {"id": "slug_page_coursepage", "label": "CoursePage()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/courses/[slug]/page.tsx", "source_location": "L9"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_app_student_courses_slug_page_tsx", "target": "users_dementiy_documents_claude_lms_system_src_lib_prisma_ts", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/courses/[slug]/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_student_courses_slug_page_tsx", "target": "lib_prisma_prisma", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/courses/[slug]/page.tsx", "source_location": "L1", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_student_courses_slug_page_tsx", "target": "navigation", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/courses/[slug]/page.tsx", "source_location": "L2", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_student_courses_slug_page_tsx", "target": "navigation", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/courses/[slug]/page.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_student_courses_slug_page_tsx", "target": "slug_page_props", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/courses/[slug]/page.tsx", "source_location": "L5", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_app_student_courses_slug_page_tsx", "target": "slug_page_coursepage", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/courses/[slug]/page.tsx", "source_location": "L9", "weight": 1.0}], "raw_calls": [{"caller_nid": "slug_page_coursepage", "callee": "findUnique", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/courses/[slug]/page.tsx", "source_location": "L12"}, {"caller_nid": "slug_page_coursepage", "callee": "notFound", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/courses/[slug]/page.tsx", "source_location": "L29"}, {"caller_nid": "slug_page_coursepage", "callee": "redirect", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/app/(student)/courses/[slug]/page.tsx", "source_location": "L34"}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_dementiy_documents_claude_lms_system_src_components_admin_csv_exporter_tsx", "label": "csv-exporter.tsx", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/csv-exporter.tsx", "source_location": "L1"}, {"id": "admin_csv_exporter_course", "label": "Course", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/csv-exporter.tsx", "source_location": "L6"}, {"id": "admin_csv_exporter_inputstyle", "label": "inputStyle", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/csv-exporter.tsx", "source_location": "L8"}, {"id": "admin_csv_exporter_focushandlers", "label": "focusHandlers", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/csv-exporter.tsx", "source_location": "L17"}, {"id": "admin_csv_exporter_csvexporter", "label": "CsvExporter()", "file_type": "code", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/csv-exporter.tsx", "source_location": "L24"}], "edges": [{"source": "users_dementiy_documents_claude_lms_system_src_components_admin_csv_exporter_tsx", "target": "react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/csv-exporter.tsx", "source_location": "L3", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_csv_exporter_tsx", "target": "lucide_react", "relation": "imports_from", "context": "import", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/csv-exporter.tsx", "source_location": "L4", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_csv_exporter_tsx", "target": "admin_csv_exporter_course", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/csv-exporter.tsx", "source_location": "L6", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_csv_exporter_tsx", "target": "admin_csv_exporter_inputstyle", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/csv-exporter.tsx", "source_location": "L8", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_csv_exporter_tsx", "target": "admin_csv_exporter_focushandlers", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/csv-exporter.tsx", "source_location": "L17", "weight": 1.0}, {"source": "users_dementiy_documents_claude_lms_system_src_components_admin_csv_exporter_tsx", "target": "admin_csv_exporter_csvexporter", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/csv-exporter.tsx", "source_location": "L24", "weight": 1.0}], "raw_calls": [{"caller_nid": "admin_csv_exporter_csvexporter", "callee": "useState", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/csv-exporter.tsx", "source_location": "L25"}, {"caller_nid": "admin_csv_exporter_csvexporter", "callee": "useState", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/csv-exporter.tsx", "source_location": "L26"}, {"caller_nid": "admin_csv_exporter_csvexporter", "callee": "useState", "is_member_call": false, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/csv-exporter.tsx", "source_location": "L27"}, {"caller_nid": "admin_csv_exporter_csvexporter", "callee": "map", "is_member_call": true, "source_file": "/Users/dementiy/Documents/Claude/lms-system/src/components/admin/csv-exporter.tsx", "source_location": "L60"}]}

Some files were not shown because too many files have changed in this diff Show More