Compare commits
219
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8223cbb963 | ||
|
|
273cb2d1b4 | ||
|
|
5ad0bd98f1 | ||
|
|
03aaec76c2 | ||
|
|
a92592ec63 | ||
|
|
8fc9708233 | ||
|
|
20a25695bc | ||
|
|
005cd329b2 | ||
|
|
917253990b | ||
|
|
2498d0f8da | ||
|
|
08e212365d | ||
|
|
92fd88964e | ||
|
|
33c0d3cafd | ||
|
|
5d94949810 | ||
|
|
98cc023709 | ||
|
|
53caf54b03 | ||
|
|
4734b99bea | ||
|
|
118fa3961f | ||
|
|
1ccf994112 | ||
|
|
02b16e311b | ||
|
|
f143f58cea | ||
|
|
fd40b1e394 | ||
|
|
0cbea10e1a | ||
|
|
668dd29397 | ||
|
|
9ad4d762d7 | ||
|
|
c50a295290 | ||
|
|
0c56b0b809 | ||
|
|
89383fab1d | ||
|
|
ef46cdbe29 | ||
|
|
b49680c118 | ||
|
|
1d5b4f9251 | ||
|
|
6316c09993 | ||
|
|
9a74339087 | ||
|
|
2c619be043 | ||
|
|
52073fd914 | ||
|
|
f5768331cf | ||
|
|
541853bb1d | ||
|
|
e795c4d655 | ||
|
|
aaf84ea931 | ||
|
|
16b17685f6 | ||
|
|
5b81a7b594 | ||
|
|
980c4d621e | ||
|
|
8c2bd42be3 | ||
|
|
fa7c25465c | ||
|
|
1133557a62 | ||
|
|
a6835567f1 | ||
|
|
c2f84079ac | ||
|
|
f8f246ca9b | ||
|
|
d2094c9541 | ||
|
|
7c8e72cd94 | ||
|
|
d20e277535 | ||
|
|
47e301a5b8 | ||
|
|
39e2102a18 | ||
|
|
3e2c70dd51 | ||
|
|
a239607360 | ||
|
|
6317dade9e | ||
|
|
1aa99b9def | ||
|
|
b6d555ab3b | ||
|
|
f8aa27533c | ||
|
|
0a1425301d | ||
|
|
06b8d9f64b | ||
|
|
1cde567dbe | ||
|
|
0e67a53122 | ||
|
|
7bf3935c81 | ||
|
|
bb6c806cdd | ||
|
|
9fbb7aea6c | ||
|
|
2436f77de5 | ||
|
|
a9869bfac8 | ||
|
|
4b300f3466 | ||
|
|
a8cd2cd4d4 | ||
|
|
2ea9c6fda0 | ||
|
|
ec011ab7fd | ||
|
|
27027b134f | ||
|
|
04e8377dab | ||
|
|
5d4630519c | ||
|
|
a7abf454d9 | ||
|
|
5a67ac8086 | ||
|
|
85cf8be705 | ||
|
|
2790a7d24b | ||
|
|
619ae393bd | ||
|
|
8240db5d22 | ||
|
|
05f46ec830 | ||
|
|
3b8a1ce7ee | ||
|
|
a290b9495c | ||
|
|
8c9f1e0a55 | ||
|
|
ffc5905309 | ||
|
|
3ed5540f40 | ||
|
|
7dc3412903 | ||
|
|
7c85a7835b | ||
|
|
ecf48de7e8 | ||
|
|
ecbca89cef | ||
|
|
cf942d223e | ||
|
|
adc1d6fc06 | ||
|
|
6624a0c535 | ||
|
|
a65125a239 | ||
|
|
14b1eacf0d | ||
|
|
d9e3531c49 | ||
|
|
5e5f616888 | ||
|
|
4222834539 | ||
|
|
2525ca2b32 | ||
|
|
cfd3681979 | ||
|
|
c2c2190cb6 | ||
|
|
6542f9b6b6 | ||
|
|
231ed083be | ||
|
|
c1c33a0619 | ||
|
|
7e6ad8067c | ||
|
|
287347168d | ||
|
|
f3428420e2 | ||
|
|
38267fa432 | ||
|
|
30405e768b | ||
|
|
3b57b14d9b | ||
|
|
367764b71e | ||
|
|
acf7ee49aa | ||
|
|
751c012f3d | ||
|
|
7084806aac | ||
|
|
b2fa98051f | ||
|
|
4f5b5c535a | ||
|
|
e5ba94cb33 | ||
|
|
12e1785ff2 | ||
|
|
bd1e77c2a3 | ||
|
|
d2362a3f1e | ||
|
|
3a2f64d47d | ||
|
|
d32186c101 | ||
|
|
f7d428180b | ||
|
|
c5d2caa345 | ||
|
|
89d614fa00 | ||
|
|
a9e6272d2d | ||
|
|
f2946db57a | ||
|
|
9cb56b9b04 | ||
|
|
6fa49d4113 | ||
|
|
90f155d334 | ||
|
|
d47f79be1a | ||
|
|
ec128f670a | ||
|
|
a27089bc0c | ||
|
|
c94a8dafa9 | ||
|
|
47840901c5 | ||
|
|
e3e6c713d2 | ||
|
|
77016a03c7 | ||
|
|
c1ae048c14 | ||
|
|
799117d287 | ||
|
|
c445bfacad | ||
|
|
41871a1e8e | ||
|
|
444b9c0faf | ||
|
|
5547b427bb | ||
|
|
2dfc42821c | ||
|
|
33dcf9bb4a | ||
|
|
a5e7b20699 | ||
|
|
93e74951a7 | ||
|
|
48721759d3 | ||
|
|
4f3b389f05 | ||
|
|
628226151b | ||
|
|
9a21c705b7 | ||
|
|
7888a7598b | ||
|
|
c25369b766 | ||
|
|
6b5bfc853e | ||
|
|
e691124058 | ||
|
|
fdb9f96382 | ||
|
|
c64f393a7b | ||
|
|
ba0a630fd9 | ||
|
|
2468671d82 | ||
|
|
7242a989ba | ||
|
|
d2150153df | ||
|
|
3ed7bc147b | ||
|
|
39d84a3db2 | ||
|
|
15df731e37 | ||
|
|
bfa037885f | ||
|
|
8757537344 | ||
|
|
65aa669522 | ||
|
|
f4e74b38d4 | ||
|
|
c050c005e4 | ||
|
|
af1fb6f61e | ||
|
|
09e5653191 | ||
|
|
29f6533e63 | ||
|
|
4821764a4f | ||
|
|
5dfa79d357 | ||
|
|
9eb21e3ab4 | ||
|
|
af8644ebce | ||
|
|
0bde11b86e | ||
|
|
d8be6d6d95 | ||
|
|
9731fcab48 | ||
|
|
0e4f6c4b01 | ||
|
|
dd198349fb | ||
|
|
808bcadfca | ||
|
|
ab37af59f2 | ||
|
|
ce305eab58 | ||
|
|
e590f541b3 | ||
|
|
48a9398905 | ||
|
|
3855bbd4be | ||
|
|
768a38b9d3 | ||
|
|
f0024c4243 | ||
|
|
d0ba4bf909 | ||
|
|
dd46a10c20 | ||
|
|
99c143d670 | ||
|
|
58a61d6f04 | ||
|
|
e77588deb8 | ||
|
|
093e403f5f | ||
|
|
66b311f17e | ||
|
|
32b0fa9d6f | ||
|
|
c647b29712 | ||
|
|
6d93a7b406 | ||
|
|
97f4c1ec24 | ||
|
|
ec51dd34bb | ||
|
|
b40d518b74 | ||
|
|
6975a9f97e | ||
|
|
9bc18247df | ||
|
|
543d5b2d5e | ||
|
|
d0c8c6dd53 | ||
|
|
c88b5d2004 | ||
|
|
4183a912e4 | ||
|
|
07b9a6d261 | ||
|
|
05dd4d1df2 | ||
|
|
03e3972388 | ||
|
|
8fdc67b4a5 | ||
|
|
e9eff5bae5 | ||
|
|
992763aeb9 | ||
|
|
09325187f9 | ||
|
|
01a9ef482c | ||
|
|
d356dddc96 | ||
|
|
9d82b73e58 |
@@ -5,7 +5,6 @@
|
||||
.env.production
|
||||
node_modules
|
||||
.next
|
||||
src/generated
|
||||
*.md
|
||||
docker-compose.yml
|
||||
docker-compose.prod.yml
|
||||
|
||||
+39
-2
@@ -12,5 +12,42 @@ S3_ACCESS_KEY=""
|
||||
S3_SECRET_KEY=""
|
||||
S3_REGION="eu-central"
|
||||
|
||||
# Kinescope (добавить при получении платного плана)
|
||||
# KINESCOPE_API_KEY=""
|
||||
# Cloudflare Turnstile (anti-bot защита на регистрации)
|
||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY=""
|
||||
TURNSTILE_SECRET_KEY=""
|
||||
|
||||
# Яндекс.Метрика — ID счётчика school.second-brain.ru (цели signup_free/gate_view).
|
||||
# Build-time (NEXT_PUBLIC_*) — передавать через --build-arg при сборке. Сам счётчик
|
||||
# (ym init snippet) ставится в admin → Settings → headCode, отдельно от этого ID.
|
||||
NEXT_PUBLIC_METRIKA_COUNTER_ID=""
|
||||
|
||||
# Kinescope API
|
||||
KINESCOPE_API_KEY=""
|
||||
|
||||
# Internal access-provisioning (payment-router → /api/internal/grant)
|
||||
INTERNAL_GRANT_SECRET=""
|
||||
|
||||
# Freemium: slug бесплатного курса, в который авто-записываем новых юзеров при регистрации
|
||||
FREE_COURSE_SLUG=obsidian-start
|
||||
|
||||
# Freemium quiz funnel (Typebot → /api/internal/quiz-lead)
|
||||
QUIZ_LEAD_SECRET=""
|
||||
# Listmonk API base — публичный домен (Listmonk живёт на Hetzner, наружу через newsletter.)
|
||||
LISTMONK_URL="https://newsletter.second-brain.ru"
|
||||
LISTMONK_USER="apiuser"
|
||||
LISTMONK_TOKEN=""
|
||||
LISTMONK_QUIZ_LIST_ID=""
|
||||
|
||||
# Freemium gate: ID wow-урока (2.6 «ежедневные заметки») — после его завершения
|
||||
# показываем лестницу-оффер (трипвайр + полный курс)
|
||||
WOW_MOMENT_LESSON_ID="obs-start-l2-006"
|
||||
|
||||
# Obsidian Toolbox (/tools): "true" — показывать точки входа и роуты (staging);
|
||||
# не задано/иное — скрыто, /tools редиректит на /dashboard (prod, пока дорабатываем)
|
||||
TOOLBOX_VISIBLE=""
|
||||
|
||||
# Чистый PDF (browserless на staging/prod; локально — SSH-туннель на staging)
|
||||
BROWSER_WS_URL="ws://localhost:3333"
|
||||
# Секрет browserless (TOKEN в его env) — на прод/staging генерировать: openssl rand -hex 24
|
||||
BROWSERLESS_TOKEN=""
|
||||
PDF_MONTHLY_LIMIT="100"
|
||||
|
||||
@@ -45,3 +45,12 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
|
||||
/src/generated/prisma
|
||||
|
||||
# Claude Code local plugins (external git repos, не коммитим)
|
||||
.claude/plugins/
|
||||
|
||||
# agent-browser auth state
|
||||
lms-auth.json
|
||||
|
||||
# SDD scratch
|
||||
.superpowers/
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Auto-generated Prisma client — раздувает граф 28 псевдо-сообществами
|
||||
src/generated/
|
||||
|
||||
# Superpowers / Claude Code инфраструктура, к LMS-коду не относится
|
||||
.claude/
|
||||
|
||||
# Build/cache артефакты (на всякий случай, помимо .gitignore)
|
||||
.next/
|
||||
out/
|
||||
build/
|
||||
coverage/
|
||||
@@ -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
|
||||
@@ -0,0 +1,62 @@
|
||||
# Quiz-Lead Task 3 — Implementation Report
|
||||
|
||||
Date: 20260622
|
||||
|
||||
## STATUS: DONE
|
||||
|
||||
## Files created/modified
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `src/app/api/internal/quiz-lead/route.ts` | CREATED |
|
||||
| `.env.example` | MODIFIED — added QUIZ_LEAD_SECRET, LISTMONK_URL, LISTMONK_USER, LISTMONK_TOKEN, LISTMONK_QUIZ_LIST_ID |
|
||||
| `src/middleware.ts` | NOT MODIFIED — `/api/internal` already in PUBLIC_ROUTES with startsWith match; covers /api/internal/quiz-lead without changes |
|
||||
|
||||
## Test infra
|
||||
|
||||
`grep -E 'vitest|jest' package.json` → no output (empty).
|
||||
No test framework present. Verification done via `npx tsc --noEmit` + manual reasoning.
|
||||
|
||||
## Commands and output
|
||||
|
||||
### `npm run type-check`
|
||||
```
|
||||
> lms-sb@0.1.0 type-check
|
||||
> tsc --noEmit
|
||||
|
||||
(no output = clean)
|
||||
```
|
||||
Exit code: 0
|
||||
|
||||
### `npm run lint` (new file only)
|
||||
```
|
||||
grep 'quiz-lead' in lint output → no results (no errors in new file)
|
||||
```
|
||||
|
||||
Pre-existing lint errors in repo (36 total, 20 errors) are NOT related to this task:
|
||||
- `.claude/plugins/superpowers/` scripts using CommonJS require
|
||||
- `quick-enroll-modal.tsx` — react-hooks/immutability (pre-existing)
|
||||
- `kinescope-player.tsx` — react-hooks/set-state-in-effect (pre-existing)
|
||||
|
||||
## Commit
|
||||
|
||||
Base: `9fbb7ae`
|
||||
New: `5e65ad6`
|
||||
|
||||
```
|
||||
5e65ad6 Add quiz-lead internal endpoint for Typebot → Listmonk funnel
|
||||
```
|
||||
|
||||
## Implementation notes
|
||||
|
||||
- Auth: `x-quiz-secret` header vs `QUIZ_LEAD_SECRET` env var; 401 on mismatch/missing
|
||||
- Validation: email regex, archetype against const tuple, Number.isFinite(score); 400 on failure
|
||||
- Upsert logic: POST create → 409 → GET by email SQL query → PUT update; 502 on Listmonk errors
|
||||
- Middleware: `/api/internal` already in PUBLIC_ROUTES with `startsWith` — no changes needed
|
||||
- TypeScript strict mode: no `any` used; typed interfaces for Listmonk response shapes
|
||||
- No production Listmonk list created; LISTMONK_QUIZ_LIST_ID left as empty placeholder in .env.example
|
||||
- No git push; no deploy
|
||||
|
||||
## Concerns
|
||||
|
||||
None. The pre-existing lint errors in the repo are unrelated to this task and were already present before this commit.
|
||||
@@ -1,5 +1,306 @@
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
# This is NOT the Next.js you know
|
||||
# AGENTS.md — LMS Second Brain
|
||||
|
||||
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.
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
Собственная LMS-платформа для образовательных курсов по PKM и Obsidian.
|
||||
Заменяет emdesell.ru. Масштаб: ~1000 аккаунтов, ~200 активных, до 10 курсов.
|
||||
Production: **https://school.second-brain.ru**
|
||||
|
||||
> Подробная техническая документация — в `TECHNICAL.md`.
|
||||
> Роадмап и текущий статус — в `ROADMAP.md`.
|
||||
> Полные правила для Claude Code — в `CLAUDE.md`.
|
||||
|
||||
---
|
||||
|
||||
## Стек
|
||||
|
||||
| Слой | Технология | Версия |
|
||||
|------|-----------|--------|
|
||||
| Фреймворк | Next.js (App Router) | **16.2.2** |
|
||||
| Язык | TypeScript (strict) | 5.x |
|
||||
| UI | React | 19 |
|
||||
| Стили | Tailwind CSS (CSS-based, **без** tailwind.config.ts) | 4.x |
|
||||
| Компоненты | shadcn/ui (Base UI, **не Radix**) | v4 |
|
||||
| ORM | Prisma | 7.x |
|
||||
| Auth | Better Auth (**не NextAuth**) | 1.6.0 |
|
||||
| Редактор | TipTap WYSIWYG | 2.x |
|
||||
| Drag-and-drop | @dnd-kit | latest |
|
||||
| БД | PostgreSQL | 16 |
|
||||
| Email | Resend | latest |
|
||||
| Хранилище | Hetzner Object Storage (S3-совместимый) | — |
|
||||
| Видео | Kinescope (iframe embed) | — |
|
||||
| Валидация | Zod | 3.x |
|
||||
|
||||
---
|
||||
|
||||
## Критические отличия от стандартных версий
|
||||
|
||||
Эти технологии отличаются от того, что содержится в обучающих данных большинства моделей. **Читай документацию перед написанием кода.**
|
||||
|
||||
### Next.js 16.2.2
|
||||
- Защита маршрутов — `src/proxy.ts` **НЕ существует**: весь код в `src/middleware.ts`, экспорт называется `middleware` (проверено 20260912). Next.js 16 поддерживает `proxy.ts`, но в этом проекте он не заведён — не создавать второй файл, править `middleware.ts`
|
||||
- Перед написанием кода смотри `node_modules/next/dist/docs/`
|
||||
|
||||
### Tailwind CSS v4
|
||||
- **Нет файла `tailwind.config.ts`** — вся кастомизация через CSS
|
||||
- Конфиг: `@import "tailwindcss"` и `@theme` в `globals.css`
|
||||
|
||||
### shadcn/ui v4
|
||||
- Базируется на `@base-ui/react`, **не Radix**
|
||||
- Нет пропа `asChild` — триггеры обычные элементы
|
||||
- Установка: `npx shadcn@latest add <component>`
|
||||
|
||||
### Prisma 7.x
|
||||
- Импорт: `from "@/generated/prisma/client"` (не `from "@/generated/prisma"`)
|
||||
- Требует адаптер: `new PrismaPg({ connectionString })`
|
||||
- Не генерирует `index.ts`
|
||||
|
||||
### Better Auth 1.6.0
|
||||
- **Не путать с NextAuth** — другая библиотека, другое API
|
||||
- В этом проекте используется **bcrypt** (не scrypt по умолчанию)
|
||||
- Настройки `password.hash` / `password.verify` в `src/lib/auth.ts`
|
||||
- `auth-client.ts` не использует `baseURL` — берёт `window.location.origin`
|
||||
- Seed-пользователи вставлены через SQL с `emailVerified = true`
|
||||
|
||||
---
|
||||
|
||||
## Команды
|
||||
|
||||
```bash
|
||||
# Разработка
|
||||
npm run dev # localhost:3000
|
||||
docker compose up -d # Поднять PostgreSQL локально
|
||||
|
||||
# Проверка качества
|
||||
npm run lint # ESLint
|
||||
npm run type-check # tsc --noEmit
|
||||
|
||||
# Сборка
|
||||
npm run build
|
||||
npm run start
|
||||
|
||||
# База данных
|
||||
npx prisma migrate dev --name <snake_case_name> # Новая миграция
|
||||
npx prisma migrate deploy # Применить в production
|
||||
npx prisma generate # Пересоздать клиент
|
||||
npx prisma db seed # Заполнить тестовыми данными
|
||||
npx prisma studio # GUI для БД
|
||||
|
||||
# Production-деплой (на сервере в /root/digital-household/lms-sb/)
|
||||
git pull
|
||||
docker compose -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
При старте production-контейнер автоматически запускает `prisma migrate deploy`, затем `node server.js`.
|
||||
|
||||
---
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
lms-system/
|
||||
├── src/
|
||||
│ ├── app/ # Next.js App Router
|
||||
│ │ ├── (auth)/ # login, register, verify-email
|
||||
│ │ ├── (student)/ # dashboard, courses/[slug], lessons/[lessonId]
|
||||
│ │ ├── curator/ # homework review, dashboard
|
||||
│ │ ├── admin/ # courses, users, settings, categories
|
||||
│ │ └── api/ # REST endpoints + Better Auth handler
|
||||
│ ├── components/
|
||||
│ │ ├── ui/ # shadcn/ui (автогенерация, не трогать)
|
||||
│ │ ├── editor/ # TipTap WYSIWYG
|
||||
│ │ ├── player/ # Kinescope Player wrapper
|
||||
│ │ ├── course/ # Компоненты курса
|
||||
│ │ └── layout/ # Header, Sidebar, Footer
|
||||
│ ├── lib/
|
||||
│ │ ├── auth.ts # Better Auth config (сервер)
|
||||
│ │ ├── auth-client.ts # Better Auth client (браузер)
|
||||
│ │ ├── prisma.ts # Prisma singleton
|
||||
│ │ ├── s3.ts # Hetzner S3 клиент
|
||||
│ │ ├── email.ts # Resend email helpers
|
||||
│ │ └── utils.ts # cn() и утилиты
|
||||
│ ├── types/ # TypeScript-типы
|
||||
│ └── middleware.ts # Auth middleware (защита маршрутов) + PUBLIC_ROUTES
|
||||
├── prisma/
|
||||
│ ├── schema.prisma # Схема БД (~314 строк)
|
||||
│ ├── seed.ts # Тестовые данные
|
||||
│ └── migrations/ # НЕ РЕДАКТИРОВАТЬ ВРУЧНУЮ
|
||||
├── docker-compose.yml # Локальная разработка
|
||||
├── docker-compose.prod.yml # Production
|
||||
├── Dockerfile # Multi-stage build
|
||||
├── .env.example # Шаблон переменных (без секретов)
|
||||
└── .env.local # Локальные секреты (в .gitignore)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Роли и маршруты
|
||||
|
||||
| Роль | Маршруты | Описание |
|
||||
|------|---------|----------|
|
||||
| `admin` | `/admin/*`, `/curator/*`, всё | Полный доступ |
|
||||
| `curator` | `/curator/*`, `/dashboard` | Проверка ДЗ, комментарии |
|
||||
| `student` | `/dashboard`, `/courses/*` | Просмотр курсов, прогресс |
|
||||
|
||||
Защита маршрутов — в `src/middleware.ts` (массив `PUBLIC_ROUTES`, сверка по `startsWith`) + проверка сессии в layout/page.
|
||||
|
||||
---
|
||||
|
||||
## Модель данных (ключевые сущности)
|
||||
|
||||
```
|
||||
User → Session, Account, Verification # Better Auth
|
||||
Category → Course → Module → Lesson # Структура контента
|
||||
Lesson → LessonFile # Файлы к уроку
|
||||
CourseEnrollment (userId + courseId) # Доступ с expiresAt
|
||||
AccessLog # Аудит доступов
|
||||
LessonProgress (userId + lessonId) # Прогресс ученика
|
||||
Lesson → Homework → HomeworkSubmission → HomeworkFeedback # ДЗ
|
||||
Lesson → LessonComment # Обсуждения (soft-delete)
|
||||
Lesson → Quiz → QuizQuestion → QuizOption # Тесты
|
||||
Quiz → QuizAttempt # Результаты тестов
|
||||
Settings (key-value) # Настройки платформы
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Дизайн-система «Second Brain Aubade»
|
||||
|
||||
Типографский, монохромный, газетный стиль.
|
||||
|
||||
| Токен | Значение |
|
||||
|-------|---------|
|
||||
| Шрифт | Fira Mono (400/500/700, Latin + Cyrillic) |
|
||||
| Фон | `#F5F5F0` (тёплый off-white) |
|
||||
| Текст | `#323232` |
|
||||
| Поверхность | `#E8E8E0` |
|
||||
| Акцент | `#E8F0D8` (зелёный) |
|
||||
| Border | `#AAAAAA` |
|
||||
| Сайдбар | `#2A2A28` (тёмный) |
|
||||
|
||||
**Aubade-эффект** (карточки и кнопки):
|
||||
- Border: `2px solid #AAAAAA`
|
||||
- Shadow: `4px 4px 0 0 #AAAAAA`
|
||||
- Hover: `translate(-2px, -2px)` + shadow `6px 6px`
|
||||
- Active: `translate(2px, 2px)`, shadow убирается
|
||||
- CSS-классы: `.card-aubade`, `.btn-aubade`, `.btn-aubade-accent`, `.tag-aubade`
|
||||
|
||||
---
|
||||
|
||||
## Инфраструктура
|
||||
|
||||
| Компонент | Значение |
|
||||
|-----------|---------|
|
||||
| Сервер | Hetzner VPS — 8 vCPU / 16 GB RAM / 320 GB NVMe |
|
||||
| Reverse proxy | Caddy (auto HTTPS, Let's Encrypt) |
|
||||
| Порт | 3010 (внутри контейнера 3000) |
|
||||
| БД | PostgreSQL 16 (контейнер `lms-sb-db-1`) |
|
||||
| Object Storage | Hetzner S3, endpoint `nbg1.your-objectstorage.com`, бакет `second-brain-lms` |
|
||||
| Git | Gitea — `https://git.second-brain.ru/admins/lms-sb` |
|
||||
| Email | Resend, домен `mailsend.second-brain.ru` |
|
||||
| Бэкапы | PostgreSQL → Backblaze B2 (ежедневно, 03:00, ротация 7 дней) |
|
||||
|
||||
---
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
```env
|
||||
DATABASE_URL="postgresql://lms_user:password@localhost:5432/lms_db"
|
||||
BETTER_AUTH_SECRET="generate-with-openssl-rand-base64-32"
|
||||
BETTER_AUTH_URL="http://localhost:3000"
|
||||
RESEND_API_KEY=""
|
||||
EMAIL_FROM="noreply@mailsend.second-brain.ru"
|
||||
S3_ENDPOINT="https://nbg1.your-objectstorage.com"
|
||||
S3_BUCKET="second-brain-lms"
|
||||
S3_ACCESS_KEY=""
|
||||
S3_SECRET_KEY=""
|
||||
S3_REGION="eu-central"
|
||||
```
|
||||
|
||||
Секреты — **только в `.env.local`**. При добавлении новых переменных обновлять `.env.example`.
|
||||
|
||||
---
|
||||
|
||||
## Правила написания кода
|
||||
|
||||
### Языки
|
||||
- **UI-строки** (заголовки, кнопки, сообщения): русский
|
||||
- **Переменные, функции, файлы, комментарии**: английский
|
||||
- **Коммиты**: английский, imperative mood (`Add lesson progress`, `Fix auth redirect`)
|
||||
|
||||
### Стиль
|
||||
- Server Actions для форм и мутаций
|
||||
- Не добавлять абстракции «на будущее» — только текущий этап
|
||||
- Нет `console.log` в production (только `console.error` для реальных ошибок)
|
||||
- Нет захардкоженных секретов, URL, ID
|
||||
|
||||
### Миграции БД
|
||||
- **Никогда** не редактировать `prisma/migrations/` вручную
|
||||
- **Всегда** спрашивать перед миграцией, которая меняет или удаляет существующие поля
|
||||
- Имена миграций: английский, snake_case (`add_lesson_progress`)
|
||||
- Перед `prisma migrate deploy` на production — бэкап БД
|
||||
|
||||
### Файлы и загрузки
|
||||
- Все файлы (ДЗ, PDF, изображения) — через Hetzner Object Storage, **не на диск VPS**
|
||||
- Обложки курсов: 16:9, max 5 MB, JPG/PNG/WebP
|
||||
- Изображения в уроках: max 10 MB
|
||||
- Файлы к уроку: max 100 MB, PDF/ZIP/DOCX
|
||||
|
||||
### Коммиты
|
||||
- Один коммит = одна логически завершённая единица
|
||||
- Перед коммитом: `npm run lint && npm run type-check`
|
||||
- После завершения каждого этапа ROADMAP — `git push` в Gitea
|
||||
|
||||
---
|
||||
|
||||
## Чек-лист перед коммитом
|
||||
|
||||
- [ ] `npm run lint` — без ошибок
|
||||
- [ ] `npm run type-check` — без ошибок
|
||||
- [ ] Новые `.env` переменные добавлены в `.env.example`
|
||||
- [ ] Миграция БД согласована (если есть)
|
||||
- [ ] Нет `console.log`, нет секретов в коде
|
||||
|
||||
---
|
||||
|
||||
## Тестовые аккаунты
|
||||
|
||||
| Email | Пароль | Роль |
|
||||
|-------|--------|------|
|
||||
| admin@second-brain.ru | Password123! | admin |
|
||||
| curator@second-brain.ru | Password123! | curator |
|
||||
| student@second-brain.ru | Password123! | student |
|
||||
|
||||
---
|
||||
|
||||
## Текущий статус проекта
|
||||
|
||||
**Завершено (9 из 13 этапов):**
|
||||
- Этап 0: Каркас, auth, роли, деплой
|
||||
- Этап 1: Курсы → Модули → Уроки (CRUD, drag-and-drop, TipTap, S3)
|
||||
- Этап 1.5: Расширенный доступ (сроки, категории, AccessLog)
|
||||
- Этап 2: Kinescope-интеграция, рендер уроков для ученика
|
||||
- Этап 3: Прогресс (кнопка завершения, прогресс-бар)
|
||||
- Этап 5: Домашние задания + обратная связь куратора
|
||||
- Этап 6: Обсуждения под уроками
|
||||
- Этап 7: Email-уведомления (Resend)
|
||||
- Этап 8: Импорт уроков из Markdown (Obsidian)
|
||||
|
||||
**В работе:**
|
||||
- Этап 9: Настройки платформы (Admin Settings)
|
||||
|
||||
**Впереди:**
|
||||
- Этап 11: Импорт/экспорт учеников (CSV, миграция с emdesell)
|
||||
- Этап 12: Telegram-бот + аналитика (Yandex.Metrika)
|
||||
- Этап 13: Тесты и квизы с автопроверкой
|
||||
|
||||
**Бэклог:** сертификаты, геймификация, платежи, медиатека, цифровой сад, CI/CD
|
||||
|
||||
Полный роадмап с деталями и критериями готовности — в `ROADMAP.md`.
|
||||
|
||||
---
|
||||
|
||||
## Известные ограничения
|
||||
|
||||
- Seed-пользователи вставлены через SQL с `emailVerified = true` (обход Better Auth)
|
||||
- Загрузка файлов: нет лимита на уровне Next.js (только S3)
|
||||
- Drag-and-drop: возможны race conditions при быстрых перетаскиваниях (некритично)
|
||||
- `expiresAt` проверяется в UI, но не блокирует доступ на уровне middleware
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
| TypeScript | 5.x | Язык |
|
||||
| React | 19 | UI |
|
||||
| PostgreSQL | 16 | База данных |
|
||||
| Prisma | 6.x | ORM + миграции |
|
||||
| Prisma | 7.x | ORM + миграции |
|
||||
| Better Auth | latest | Аутентификация и сессии |
|
||||
| Tailwind CSS | 4.x (CSS-based config) | Стили (нет tailwind.config.ts) |
|
||||
| shadcn/ui | latest | UI-компоненты |
|
||||
| TipTap | 2.x | WYSIWYG-редактор уроков |
|
||||
| @kinescope/react-kinescope-player | latest | Видеоплеер |
|
||||
| Resend | latest | Email-уведомления |
|
||||
| AWS SDK (S3) | 3.x | Hetzner Object Storage |
|
||||
| AWS SDK (S3) | 3.x | Backblaze B2 (раздача через Bunny CDN) |
|
||||
| Zod | 3.x | Валидация данных |
|
||||
| Docker Compose | 2.x | Локальная разработка и деплой |
|
||||
|
||||
@@ -66,7 +66,7 @@ lms-system/
|
||||
│ │ ├── auth.ts # Better Auth config (сервер)
|
||||
│ │ ├── auth-client.ts # Better Auth client (браузер)
|
||||
│ │ ├── prisma.ts # Prisma singleton client
|
||||
│ │ ├── s3.ts # Hetzner Object Storage клиент
|
||||
│ │ ├── s3.ts # Backblaze B2 (S3-совместимый) клиент
|
||||
│ │ ├── email.ts # Resend email helpers
|
||||
│ │ └── utils.ts # cn() и прочие утилиты
|
||||
│ ├── types/
|
||||
@@ -135,7 +135,7 @@ docker compose -f docker-compose.prod.yml logs -f app
|
||||
- Перед `prisma migrate deploy` на production — делать бэкап БД
|
||||
|
||||
### Файлы и контент
|
||||
- Загружаемые файлы (ДЗ, PDF) — только через Hetzner Object Storage, никогда на диск VPS
|
||||
- Загружаемые файлы (ДЗ, PDF, картинки комментариев) — только в Backblaze B2, никогда на диск VPS
|
||||
- Секреты (API-ключи, токены, строки подключения) — **только в `.env.local`**, в коде запрещено
|
||||
- `.env.example` всегда обновлять при добавлении новых переменных (без реальных значений)
|
||||
|
||||
@@ -168,8 +168,9 @@ BETTER_AUTH_URL="http://localhost:3000"
|
||||
RESEND_API_KEY=""
|
||||
EMAIL_FROM="noreply@school.second-brain.ru"
|
||||
|
||||
# Hetzner Object Storage (S3-совместимый)
|
||||
S3_ENDPOINT="https://fsn1.your-objectstorage.com"
|
||||
# Backblaze B2 (S3-совместимый), раздача через Bunny CDN
|
||||
S3_ENDPOINT="https://s3.eu-central-003.backblazeb2.com"
|
||||
S3_CDN_URL="https://files.second-brain.ru"
|
||||
S3_BUCKET="lms-uploads"
|
||||
S3_ACCESS_KEY=""
|
||||
S3_SECRET_KEY=""
|
||||
@@ -181,6 +182,67 @@ S3_REGION="eu-central"
|
||||
|
||||
---
|
||||
|
||||
## Грабли уклада (проверено 20260912)
|
||||
|
||||
Неочевидные соглашения, уже действующие в коде. Сломать их легко «улучшением».
|
||||
|
||||
### Тесты
|
||||
|
||||
- `vitest.config.ts` ищет тесты **только** по `src/lib/**/__tests__/**/*.test.ts`. Тест рядом с исходником или в `src/app/**` молча не подбирается — зелёный `npm test` не значит, что твой тест выполнился. Проверяй, что файл попал в прогон.
|
||||
- Интеграционный тест генерации PDF выключен по умолчанию: `describe.runIf(process.env.RUN_PDF_INTEGRATION === "1")` + нужен живой `BROWSER_WS_URL`. Гейт не снимать — тест ходит в интернет и в browserless.
|
||||
- Решения выносим в чистую функцию с внедряемым IO, обёртка с IO остаётся тонкой (`decidePaidAccess` рядом с `hasPaidAccess`; `assertPublicUrl(raw, resolve)`). Prisma и dns в тестах **не мокаем**.
|
||||
|
||||
### Маршруты
|
||||
|
||||
- **`src/proxy.ts` не существует** — вся защита маршрутов в `src/middleware.ts`, экспорт `middleware`. Не заводить второй файл.
|
||||
- Новый API-роут, который авторизуется сам (Bearer-ключ, внутренний секрет), обязан попасть в `PUBLIC_ROUTES` — иначе middleware отдаст редирект на `/login` раньше роута, и в браузере с кукой это не воспроизведётся. Обратная сторона: сверка идёт по `startsWith`, поэтому запись `/api/pdf` уже открывает любой будущий `/api/pdf-*` — такой роут обязан проверять доступ внутри себя.
|
||||
|
||||
### Чистый PDF (`/tools/clean-pdf`, `/api/pdf`)
|
||||
|
||||
- **Два разных значения `ToolUsage.tool`**: `clean-pdf-generate` (`PDF_TOOL_ID`) — только успешные генерации, по ним считаются месячный лимит и burst; `clean-pdf` — аналитика копирований из `CopyButton`. Не объединять: иначе каждое нажатие «Скопировать» списывает генерацию. `clean-pdf-generate` намеренно отсутствует в `TOOL_IDS`, чтобы публичный Server Action не мог списать квоту.
|
||||
- Квота списывается **только после успешной** генерации, месячное окно — от начала месяца по UTC (`monthStartUtc`). Рефакторинг «сначала резервируем квоту, потом рендерим» начнёт списывать за каждую неоткрывшуюся страницу.
|
||||
- `page.pdf()` в playwright-core не принимает `timeout` — оборачивать своим `withTimeout` (Promise.race), иначе зависшая печать держит слот `CONCURRENT` у browserless.
|
||||
- `context.route("**/*")` **не видит WebSocket** — нужен отдельный `context.routeWebSocket`. Два похожих блока подряд — не дублирование, это вторая линия SSRF-обороны; удалять только осознанно.
|
||||
- `isPrivateAddress(ip)` — **fail-closed гейт, а не предикат**: на всё, что не разбирается как IP, возвращает `true`. Не переиспользовать как «адрес из приватного диапазона».
|
||||
- В `resolveUserId` ветка Bearer терминальная: если заголовок `Authorization` есть, фолбэка на сессию нет ни при каком исходе. Не добавлять «если ключ не подошёл — попробуем куку»: запрос с чужим ключом из залогиненного браузера начнёт выполняться от имени владельца сессии.
|
||||
- Заголовки ответа — публичный контракт с уже розданными ключами: `X-Uses-Count`, `X-Max-Uses` и двойной `Content-Disposition` (ASCII-фолбэк + RFC 5987 `filename*`). Их читают Zotero-скрипт и форма на сайте.
|
||||
- Карта ошибок: `BlockedUrlError`/`EmptyContentError` → 422, `RenderError` → 504, остальное → 504, **никогда 500**. Текст `{error}` показывается студенту дословно — только по-русски, без внутренних деталей.
|
||||
- HTML от Defuddle санируется собственным проходом в `template.prepareContent` (JSDOM: `script`/`style`/`iframe`/`object`/`embed`, все `on*`, `javascript:`) — намеренно **не** через `rehype-sanitize`, который заточен под Markdown-пайплайн комментариев.
|
||||
- `zotero-script.ts` генерирует JS шаблонной строкой: учетверённые бэкслеши (`\\\\` в исходнике = один `\` в выданном скрипте). Ошибка экранирования не падает, а тихо ломает регулярки в выданном студенту скрипте — правил строку, прогони тест на валидность (`new Function(script)`).
|
||||
|
||||
### Env
|
||||
|
||||
- `NEXT_PUBLIC_*` инлайнятся **на сборке**. `NEXT_PUBLIC_APP_URL` задан только как runtime-env в `docker-compose.prod.yml`, в образ не попадает → работает захардкоженный фолбэк на прод-адрес. Следствие: на staging студенту выдаются Zotero-скрипт и curl-пример с **прод**-адресом. Правишь — передавай через `--build-arg`, как `NEXT_PUBLIC_TURNSTILE_SITE_KEY`.
|
||||
|
||||
### Тариф — это отдельный курс со своей копией уроков
|
||||
|
||||
`obsidian` и `obsidian-full` (как и `zotero` / `zotero-full`) — **две разные записи `Course`, у каждой свой полный комплект `Module` и `Lesson`**. Это не представления одного курса.
|
||||
|
||||
- **Правка контента применяется в обе копии.** Починил текст в «Всё включено» — в базовом тарифе он остался прежним. Копии уже расходились в проде: блок с благодарностью автору был только в дешёвом тарифе.
|
||||
- **Комментарии сыплются под уроки обеих копий.** Ответ под уроком `zotero-full` студенты `zotero` не увидят; выборка «неотвеченные» должна идти по всем курсам сразу.
|
||||
- Перед массовой правкой — `SELECT` по обоим slug, потом `UPDATE ... WHERE id IN (...)`.
|
||||
|
||||
### Markdown рендерится не везде
|
||||
|
||||
| Где | Что рендерится |
|
||||
|---|---|
|
||||
| Комментарии к урокам (`lesson-comments.tsx`) | Markdown + GFM: разметка, таблицы, голый адрес становится ссылкой |
|
||||
| Отзыв куратора на ДЗ (`homework-section.tsx`) | простой текст (`whitespace-pre-wrap` + `linkify`) |
|
||||
| Раздел вопросов (`QuestionThread.tsx`, `QuestionSplitView.tsx`) | простой текст (`whitespace-pre-wrap` + `linkify`) |
|
||||
|
||||
`**жирный**` в отзыве на ДЗ студент увидит звёздочками. Ссылку там же писать голым адресом можно — `linkify` её поднимет.
|
||||
|
||||
Безопасность рендера комментариев (не ослаблять «улучшением»): `rehype-sanitize` режет сырой HTML и `javascript:`; картинки с чужих доменов **не грузятся** — компонент `img` отдаёт их ссылкой, чтобы адрес читателя не утекал на сторонний сервер. Свои картинки — через `/api/student/comment-upload`.
|
||||
|
||||
### Файлы курсов живут на двух разных доменах
|
||||
|
||||
- `files.second-brain.ru` — Bunny CDN → Backblaze B2. Вложения уроков, ДЗ, картинки комментариев. Кеш короткий, правка «на месте» доезжает за час.
|
||||
- `filez.second-brain.ru` — Selectel CDN → origin на Hoster.kz `/root/filez-static/`. Бонусы, архивы, шаблоны и CSS курсов. ⚠️ Кеш до 30 дней, **query-параметры игнорируются**: чтобы обновление дошло сразу, публиковать под новым путём или именем, а не `?v=2`.
|
||||
|
||||
Домены различаются одной буквой — легко перепутать при диагностике.
|
||||
|
||||
---
|
||||
|
||||
## Чек-лист перед каждым коммитом
|
||||
|
||||
- [ ] `npm run lint` — нет ошибок ESLint
|
||||
|
||||
@@ -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.
|
||||
@@ -9,6 +9,11 @@ FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
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
|
||||
ARG NEXT_PUBLIC_METRIKA_COUNTER_ID
|
||||
ENV NEXT_PUBLIC_METRIKA_COUNTER_ID=$NEXT_PUBLIC_METRIKA_COUNTER_ID
|
||||
# Generate Prisma client before build
|
||||
RUN npx prisma generate
|
||||
RUN npm run build
|
||||
|
||||
+292
-142
@@ -6,13 +6,12 @@
|
||||
---
|
||||
|
||||
## Этап 0 — Каркас, auth, роли ✅ ЗАВЕРШЁН (07.04.2026)
|
||||
**Задеплоено на:** https://school.second-brain.ru
|
||||
|
||||
- [x] Next.js 16.2.2 (App Router, TypeScript, Tailwind v4)
|
||||
- [x] Docker Compose: PostgreSQL 16 (прод на Hetzner)
|
||||
- [x] Prisma 7: схема User, Session, Account + полная LMS-модель
|
||||
- [x] Better Auth: вход по email/password, роли student/curator/admin
|
||||
- [x] proxy.ts: защита маршрутов по сессии
|
||||
- [x] Middleware: защита маршрутов по сессии
|
||||
- [x] Дашборды для трёх ролей
|
||||
- [x] Страница входа, регистрации, подтверждения email
|
||||
- [x] Seed: admin/curator/student (пароль: Password123!)
|
||||
@@ -21,62 +20,249 @@
|
||||
|
||||
---
|
||||
|
||||
## Этап 1 — Курсы → Модули → Уроки (CRUD в админке)
|
||||
**Цель:** могу создать полную структуру курса из браузера.
|
||||
## Этап 1 — Курсы → Модули → Уроки (CRUD в админке) ✅ ЗАВЕРШЁН (07.04.2026)
|
||||
|
||||
- [ ] Prisma-схема: Course, Module, Lesson (с порядком, статусом published)
|
||||
- [ ] Admin: список курсов, создать / редактировать / удалить курс
|
||||
- [ ] Admin: список модулей внутри курса, drag-and-drop сортировка
|
||||
- [ ] Admin: список уроков внутри модуля, drag-and-drop сортировка
|
||||
- [ ] Admin: редактор урока с TipTap (заголовки, списки, цитаты, код, картинки, ссылки)
|
||||
- [ ] Загрузка картинок в уроке → Hetzner Object Storage
|
||||
- [ ] Поле для Kinescope ID в уроке (просто текстовое, без интеграции — это Этап 2)
|
||||
- [ ] Публикация/скрытие курса и урока (черновик / опубликован)
|
||||
- [ ] Управление доступом: выдать / забрать доступ к курсу для пользователя
|
||||
|
||||
**Критерий готовности:** создаю курс «Obsidian PKM», добавляю 2 модуля, в каждом по 3 урока с текстом и картинкой, публикую.
|
||||
- [x] Prisma-схема: Course, Module, Lesson (с порядком, статусом published)
|
||||
- [x] Admin: список курсов, создать / редактировать / удалить курс
|
||||
- [x] Admin: список модулей внутри курса, drag-and-drop сортировка
|
||||
- [x] Admin: список уроков внутри модуля, drag-and-drop сортировка
|
||||
- [x] Admin: редактор урока с TipTap (заголовки, списки, цитаты, код, картинки, ссылки)
|
||||
- [x] Загрузка картинок в уроке → Hetzner Object Storage
|
||||
- [x] Поле для Kinescope ID в уроке
|
||||
- [x] Публикация/скрытие курса и урока (черновик / опубликован)
|
||||
- [x] Управление доступом: выдать / забрать доступ к курсу для пользователя
|
||||
- [x] Дизайн: Fira Mono, #F5F5F0, Aubade-карточки
|
||||
- [x] Admin: таблица пользователей (/admin/users)
|
||||
|
||||
---
|
||||
|
||||
## Этап 2 — Интеграция Kinescope, рендер уроков для ученика
|
||||
**Цель:** ученик видит урок с видео Kinescope и текстом.
|
||||
|
||||
- [ ] Компонент `KinescopePlayer` — обёртка над `@kinescope/react-kinescope-player`
|
||||
- [ ] Рендер урока для ученика: видео (если есть Kinescope ID) + текст + файлы
|
||||
- [ ] Загрузка PDF/файлов к уроку (Object Storage), список для скачивания
|
||||
- [ ] Страница курса для ученика: список модулей и уроков, статус прохождения
|
||||
- [ ] Навигация по урокам: предыдущий / следующий
|
||||
- [ ] Блокировка доступа к курсу без enrollment (middleware или server component)
|
||||
- [ ] Страница «Мои курсы» в личном кабинете ученика
|
||||
|
||||
**Кинескоп-нюанс:** пока без DRM (бесплатный план), просто передаём `videoId` в компонент. Когда появится платный план — добавим signed URLs в отдельной задаче.
|
||||
|
||||
**Критерий готовности:** ученик открывает урок, смотрит видео из Kinescope, читает текст, скачивает PDF.
|
||||
**Доработки таблицы пользователей (добавить в рамках Этапа 9):**
|
||||
- [ ] Фильтры: по роли (Ученик / Куратор / Администратор), по статусу email (подтверждён/нет)
|
||||
- [ ] Поиск по имени / email
|
||||
- [ ] Пагинация + выбор кол-ва записей на странице (20/50/100)
|
||||
- [ ] Ховер-попап на строке: список курсов пользователя со сроками доступа + email + телефон
|
||||
- [ ] Быстрые действия в строке: кнопка «Выдать доступ» без перехода на страницу пользователя
|
||||
|
||||
---
|
||||
|
||||
## Этап 3 — Прогресс и линейное открытие уроков
|
||||
**Цель:** ученик проходит курс последовательно, прогресс сохраняется.
|
||||
## Этап 1.5 — Расширенное управление доступом ✅ ЗАВЕРШЁН (07.04.2026)
|
||||
|
||||
- [ ] Prisma: таблица `LessonProgress` (userId, lessonId, completedAt)
|
||||
- [ ] Кнопка «Урок завершён» — создаёт запись в LessonProgress
|
||||
- [ ] Логика блокировки: следующий урок открыт только если предыдущий завершён
|
||||
- [ ] Прогресс-бар по курсу (% завершённых уроков)
|
||||
- [ ] Прогресс-бар по модулю
|
||||
- [ ] API: `POST /api/progress/complete` — принимает lessonId, создаёт прогресс
|
||||
- [ ] Иконки статуса уроков в боковой панели: ✓ пройден / 🔒 заблокирован / → текущий
|
||||
|
||||
**Примечание:** если в уроке есть тест (Этап 4) или ДЗ (Этап 5) — кнопка «Завершить» появляется только после их прохождения/отправки. Эту логику дорабатываем на соответствующих этапах.
|
||||
|
||||
**Критерий готовности:** прохожу урок 1, нажимаю «Завершён» — открывается урок 2. Урок 3 заблокирован. Прогресс-бар показывает 33%.
|
||||
- [x] Срок доступа: `expiresAt` в `CourseEnrollment`, просроченный подсвечивается красным
|
||||
- [x] Категории курсов: таблица `Category`, `/admin/categories`, привязка к курсу
|
||||
- [x] Расширенный энролл: `/admin/users/[userId]` — выбор нескольких курсов + срок одной операцией
|
||||
- [x] История доступа: `AccessLog` — каждая операция логируется (кто, когда, метод, примечание)
|
||||
|
||||
---
|
||||
|
||||
## Этап 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
|
||||
- [ ] Admin: создание теста к уроку (добавить вопрос → варианты ответов → отметить правильный)
|
||||
- [ ] Prisma: Quiz, QuizQuestion, QuizOption, QuizAttempt (схема уже есть)
|
||||
- [ ] Admin: создание теста к уроку (вопрос → варианты → отметить правильный)
|
||||
- [ ] Типы вопросов: одиночный выбор, множественный выбор, короткий текст
|
||||
- [ ] Рендер теста в уроке для ученика
|
||||
- [ ] Авто-проверка (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)
|
||||
|
||||
- **Миграция 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,38 @@
|
||||
- Kinescope DRM (signed URLs) — при переходе на платный план
|
||||
- Водяные знаки на 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 граф знаний
|
||||
|
||||
- **Markdown-рендер описаний ДЗ** — сейчас `Homework.description` выводится как plain text (`whitespace-pre-wrap` в `src/components/student/homework-section.tsx`), поэтому markdown-разметка (списки, `**жирный**`, ссылки) показывается сырой. Перевести на markdown-рендер (react-markdown или общий рендерер контента урока), чтобы задания оформлялись аккуратно.
|
||||
- Контекст: при миграции ДЗ из Emdesell были артефакты форматирования (оторванные маркеры списков `• \n`, пачки пустых строк). 17.06.2026 13 ДЗ (очаг — obsidian-ai) причёсаны regex'ом под текущий plain-text рендер (бэкап `Homework_bak_20260617`). Полноценное решение — markdown-рендер.
|
||||
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
# 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 |
|
||||
| `GET` | `/api/pdf` | Чистый PDF из URL (Bearer-ключ или сессия) — см. раздел ниже | платный студент, admin, curator |
|
||||
|
||||
---
|
||||
|
||||
## Чистый PDF (`/tools/clean-pdf`, `/api/pdf`)
|
||||
|
||||
Инструмент Obsidian Toolbox: превращает произвольный URL в чистый PDF (без рекламы и меню, с типографикой, оглавлением, A4/Letter, light/dark). Полный дизайн-документ: [`docs/specs/20260706-clean-pdf-design.md`](docs/specs/20260706-clean-pdf-design.md).
|
||||
|
||||
**Пайплайн генерации:**
|
||||
|
||||
```
|
||||
URL студента
|
||||
→ browserless (Chromium по WebSocket, playwright-core) загружает страницу
|
||||
→ Next.js забирает итоговый HTML
|
||||
→ Defuddle (JSDOM) выделяет основной контент
|
||||
→ HTML-шаблон (типографика, A4/Letter, light/dark, оглавление)
|
||||
→ Playwright page.pdf() в том же browserless
|
||||
→ application/pdf в ответе
|
||||
```
|
||||
|
||||
Доступ: любой платный студент — есть `CourseEnrollment` на курс вне `FREE_COURSE_SLUG` и не истёкший (`expiresAt` пуст или в будущем); admin/curator проходят гейт без курсов. `banned` перекрывает всё, включая admin. «Без ограничений» — только про гейт доступа: квоты (burst 5/мин и `PDF_MONTHLY_LIMIT`) считаются по `userId` одинаково для всех ролей. Видимость страницы гейтится флагом `TOOLBOX_VISIBLE`, но сам `/api/pdf` работает независимо от него (доступ проверяется отдельно). Ключ для внешнего API — модель `PdfApiKey` (`sbpdf_<random>`, ленивая генерация, регенерация инвалидирует старый).
|
||||
|
||||
**Env-переменные:**
|
||||
|
||||
| Переменная | Назначение |
|
||||
|---|---|
|
||||
| `BROWSER_WS_URL` | WebSocket-адрес browserless (`ws://browserless:3000` в compose, `ws://localhost:3333` при туннеле локально) |
|
||||
| `BROWSERLESS_TOKEN` | Секрет browserless (`TOKEN` в его env) — общий и для сервиса, и для клиента в LMS |
|
||||
| `PDF_MONTHLY_LIMIT` | Лимит генераций в месяц на студента (по умолчанию `100`), без пересборки |
|
||||
| `TOOLBOX_VISIBLE` | Видимость раздела `/tools/*` и ссылок на него. На `/api/pdf` **не влияет** |
|
||||
| `FREE_COURSE_SLUG` | Какой курс считается бесплатным (дефолт `obsidian-start`) — от него зависит, что считать платным enrollment |
|
||||
| `NEXT_PUBLIC_APP_URL` | Базовый адрес в Zotero-скрипте и curl-примере. ⚠️ `NEXT_PUBLIC_*` инлайнится **на сборке**, а задан только как runtime-env в compose → в образе работает фолбэк на прод-адрес (на staging скрипт выдаётся с прод-URL) |
|
||||
|
||||
Контейнер `browserless` (`ghcr.io/browserless/chromium`, образ **не запинен по версии**). На проде порт не публикуется вовсе — доступ только по внутренней сети compose. В dev и на staging порт публикуется на loopback хоста (`127.0.0.1:3333:3000`) — именно на этом держится SSH-туннель для интеграционного теста; наружу он при этом не торчит.
|
||||
|
||||
**SSRF-защита — честно про пределы:** исходный URL проверяется резолвом DNS (`assertPublicUrl`: блок приватных и зарезервированных диапазонов, включая v4-mapped IPv6), а внутри browserless запросы страницы фильтруются по `context.route` + `context.routeWebSocket`.
|
||||
|
||||
Предел у этой защиты шире, чем «DNS-rebind»: `assertPublicUrl` вызывается **ровно один раз — для исходного URL**, а browser-level фильтр хостнеймы **не резолвит вовсе** — сверяет только строки `localhost`/`*.local`/`*.internal` и литеральные IP. Значит без проверки уходит **любой новый хостнейм после первого перехода**: цель HTTP-редиректа, субресурс страницы, `fetch`/XHR из JS, `ws://`. Достаточно редиректа на внутреннее имя — DNS-rebind даже не требуется.
|
||||
|
||||
Закрывается это только на сетевом уровне. **Перед тем как поднять контейнер `browserless` на проде** (а не перед флагом `TOOLBOX_VISIBLE` — `/api/pdf` живёт независимо от него), нужно заблокировать исходящий трафик рендерера в `169.254.0.0/16` и RFC1918. Полный разбор — «Безопасность» и «Деплой» в [`docs/specs/20260706-clean-pdf-design.md`](docs/specs/20260706-clean-pdf-design.md) и процесс `SBT/02-Стандарты/Процессы/lms-deploy.md`.
|
||||
|
||||
---
|
||||
|
||||
## Структура БД (ключевые таблицы)
|
||||
|
||||
```
|
||||
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
|
||||
@@ -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-интеграция?
|
||||
@@ -0,0 +1,51 @@
|
||||
# Third-Party Notices
|
||||
|
||||
This product bundles third-party software. Their licenses and copyright
|
||||
notices are reproduced below as required.
|
||||
|
||||
## force-graph
|
||||
|
||||
Used client-side in the "Obsidian Wrapped" feature to render the vault graph
|
||||
poster on an off-screen canvas. The library and its node names never leave the
|
||||
browser.
|
||||
|
||||
- Version: 1.51.4
|
||||
- License: MIT
|
||||
- Copyright (c) 2018 Vasco Asturiano
|
||||
- https://github.com/vasturiano/force-graph
|
||||
|
||||
```
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Vasco Asturiano
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
force-graph depends on the d3-force family (ISC) by Mike Bostock for its
|
||||
force simulation, bundled transitively.
|
||||
|
||||
## next/og (Satori + resvg)
|
||||
|
||||
Server-side share-card PNG rendering (`ImageResponse`) uses Vercel's `next/og`,
|
||||
which bundles `satori` and `@resvg/resvg-wasm` (both MPL-2.0). These are part of
|
||||
the `next` dependency and distributed under Next.js's own license terms.
|
||||
|
||||
- satori — MPL-2.0 — https://github.com/vercel/satori
|
||||
- resvg-wasm — MPL-2.0 — https://github.com/yisibl/resvg-js
|
||||
@@ -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": {}
|
||||
}
|
||||
+21
-2
@@ -13,24 +13,43 @@ services:
|
||||
NEXT_PUBLIC_APP_URL: "https://school.second-brain.ru"
|
||||
RESEND_API_KEY: "${RESEND_API_KEY}"
|
||||
EMAIL_FROM: "${EMAIL_FROM}"
|
||||
BROWSER_WS_URL: "ws://browserless:3000"
|
||||
BROWSERLESS_TOKEN: "${BROWSERLESS_TOKEN}"
|
||||
PDF_MONTHLY_LIMIT: "${PDF_MONTHLY_LIMIT:-100}"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
browserless:
|
||||
condition: service_started
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
# ⚠️ PG18 сменил раскладку: том монтируется в /var/lib/postgresql (НЕ .../data),
|
||||
# кластер лежит в подкаталоге 18/docker. Мажор прода сделан 09.07.2026.
|
||||
# Не откатывать тег и mount по отдельности — иначе initdb на непустом томе → рестарт-луп.
|
||||
# В этом каталоге на Hetzner том lms-sb_postgres_data = hot-standby (тоже PG18).
|
||||
image: postgres:18-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: lms_user
|
||||
POSTGRES_PASSWORD: "${DB_PASSWORD}"
|
||||
POSTGRES_DB: lms_db
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- postgres_data:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U lms_user -d lms_db"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
browserless:
|
||||
image: ghcr.io/browserless/chromium
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
TOKEN: "${BROWSERLESS_TOKEN}"
|
||||
CONCURRENT: "2"
|
||||
QUEUED: "10"
|
||||
TIMEOUT: "120000"
|
||||
mem_limit: 1g
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
+13
-3
@@ -1,15 +1,25 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
image: postgres:18-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: lms_user
|
||||
POSTGRES_PASSWORD: lms_password
|
||||
POSTGRES_DB: lms_db
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- "127.0.0.1:5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- postgres_data:/var/lib/postgresql
|
||||
|
||||
browserless:
|
||||
image: ghcr.io/browserless/chromium
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:3333:3000"
|
||||
environment:
|
||||
CONCURRENT: "2"
|
||||
QUEUED: "10"
|
||||
TIMEOUT: "120000"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Чистый PDF — снимок состояния
|
||||
|
||||
**Дата снимка:** 20260912
|
||||
**Фаза:** слито в `main`, работает на staging, на прод не выкачено
|
||||
**Охват:** только фича «Чистый PDF». Состояние остальных частей LMS здесь не отражено.
|
||||
|
||||
---
|
||||
|
||||
## Коротко
|
||||
|
||||
Инструмент `URL → чистый PDF` в Obsidian Toolbox: веб-форма в кабинете, персональный API-ключ + скрипт для Zotero (Actions & Tags), открытый curl API. Рендер — отдельный контейнер browserless (Chromium по CDP), контент выделяет Defuddle, печатает `page.pdf()`.
|
||||
|
||||
Сделано 20260706 по плану из 11 задач (субагентное исполнение с ревью после каждой). Merge `53caf54`, 21 коммит. Финальное whole-branch ревью — READY TO MERGE.
|
||||
|
||||
## Состояние сред (проверено 20260912)
|
||||
|
||||
| Среда | Состояние |
|
||||
|---|---|
|
||||
| `main` | Фича на месте и цела: файлы побайтово совпадают с merge `53caf54`; `/api/pdf` в `PUBLIC_ROUTES`, регистрация инструмента и модель `PdfApiKey` не пострадали от августовских коммитов |
|
||||
| **Прод** (Hoster.kz) | **Выключено.** Контейнера `browserless` нет, `BROWSER_WS_URL`/`BROWSERLESS_TOKEN`/`TOOLBOX_VISIBLE` в `/root/lms-sb/.env` не заданы. Репозиторный `docker-compose.prod.yml` намеренно расходится с живым `/root/lms-sb/docker-compose.yml` |
|
||||
| **Staging** (Hetzner) | **Работает.** Стенд стоит ровно на `53caf54` и отстал от `main` на августовские коммиты. `TOOLBOX_VISIBLE=true`, контейнеры `lms-staging-app-1` и `lms-staging-browserless-1` живы с 20260706 |
|
||||
|
||||
Проверено E2E на staging 20260706: 401 без ключа, 403 без платного доступа, 200 с реальным PDF (576 КБ, 4 страницы), 422 на SSRF-пробы (`192.168.x`, `localhost:5432`, `[::ffff:169.254.169.254]`). Страница и перевыпуск ключа проверены в живом браузере.
|
||||
|
||||
## Что где лежит
|
||||
|
||||
- Код: `src/lib/clean-pdf/` (`ssrf.ts`, `template.ts`, `generate.ts`, `access.ts`, `keys.ts`, `api-key.ts`, `zotero-script.ts`), роут `src/app/api/pdf/route.ts`, страница `src/app/(student)/tools/clean-pdf/`, действие `src/lib/actions/pdf-key-actions.ts`
|
||||
- Данные: модель `PdfApiKey`, миграция `20260706120000_add_pdf_api_key`; учёт — существующая `ToolUsage`
|
||||
- Документы: спека `docs/specs/20260706-clean-pdf-design.md`, план `docs/superpowers/plans/20260706-clean-pdf.md`, раздел в `TECHNICAL.md`
|
||||
- Уклад и грабли: раздел «Грабли уклада» в `CLAUDE.md`
|
||||
- В базе знаний: `SBT/00-Стек/Сервисы/clean-pdf.md`, `SBT/03-Опыт/Постмортемы/20260706-clean-pdf-ssrf-обходы.md`, гейт в `SBT/02-Стандарты/Процессы/lms-deploy.md`
|
||||
|
||||
## ⚠️ Гейт перед прод-релизом
|
||||
|
||||
**Закрыть исходящий трафик контейнера `browserless`** — заблокировать `169.254.0.0/16` и RFC1918 через `internal`-сеть compose или `DOCKER-USER`. На 20260912 не сделано ни в репозитории (у сервиса нет `networks`/`internal`), ни на хосте (`DOCKER-USER` пуста).
|
||||
|
||||
**Гейт привязан к рендереру, а не к флагу.** `TOOLBOX_VISIBLE` прячет только страницы `/tools/*`; `/api/pdf` лежит в `PUBLIC_ROUTES` и авторизуется сам, поэтому фича оживёт для любого платного студента в момент появления `browserless` и `BROWSER_WS_URL` — ещё до поднятия флага.
|
||||
|
||||
Причина: `assertPublicUrl` резолвит DNS **один раз, для исходного URL**, а фильтр внутри браузера хостнеймы не резолвит вовсе. Непроверенным уходит любой новый хостнейм после первого перехода (редирект, субресурс, `fetch`, `ws://`) — DNS-rebind для обхода не нужен.
|
||||
|
||||
## Хвосты (по убыванию важности)
|
||||
|
||||
1. **Egress-изоляция browserless** — гейт выше. Блокирует прод-релиз.
|
||||
2. **Фикс WebSocket-обхода (`routeWebSocket`) не покрыт тестом.** Баг с v4-mapped IPv6 закрыт юнит-тестами `ssrf.test.ts`, а браузерный фильтр не тестируется вовсе (единственный тест генерации за `RUN_PDF_INTEGRATION`). Регрессия пройдёт молча.
|
||||
3. **Образ browserless не запинен** — `ghcr.io/browserless/chromium` без тега в обоих compose. Контейнер исполняет контент атакующего; обновление меняет Chromium и поведение CDP-слоя, на котором держатся фильтры.
|
||||
4. **`NEXT_PUBLIC_APP_URL` не доезжает до образа** — задан только как runtime-env в compose, а `NEXT_PUBLIC_*` инлайнятся на сборке. Работает захардкоженный фолбэк на прод-адрес: на staging студенту выдаются Zotero-скрипт и curl с **прод**-URL. Лечится `--build-arg`, как у `NEXT_PUBLIC_TURNSTILE_SITE_KEY`.
|
||||
5. **Ключи `PdfApiKey` лежат в БД открытым текстом** (поиск прямым `findUnique`). Дампы, реплика на standby и PITR содержат рабочие ключи. Ущерб ограничен (только генерация в пределах лимита, доступ проверяется на каждый запрос), но при релизе стоит перейти на хеш.
|
||||
6. **Лимит не расходуется неуспешными генерациями** — `ToolUsage` пишется строго после успешного рендера, поэтому ни месячная квота, ни burst не тратятся на отказы. Ограниченный self-DoS тяжёлыми URL. Рассмотреть подсчёт попыток.
|
||||
7. **`RenderError` обёрнута только часть вызовов браузера** (`connectOverCDP`, `goto`, таймаут печати). `newContext`, `newPage`, `content`, `route`/`routeWebSocket` и reject самого `pdf()` дают сырую ошибку Playwright — её ловит catch-all роута и отдаёт 504.
|
||||
|
||||
Маркеров TODO/FIXME в коде фичи нет — хвосты живут в документах.
|
||||
|
||||
## Если возвращаемся к работе
|
||||
|
||||
1. Прочитать этот файл и «Грабли уклада» в `CLAUDE.md`.
|
||||
2. Перед любым касанием прода — `SBT/02-Стандарты/Процессы/lms-deploy.md` (там гейт).
|
||||
3. Staging отстал от `main` — перед проверками подтянуть ветку и передеплоить (`bash ~/Documents/Claude/scripts/deploy-staging.sh`; скрипт иногда не пересоздаёт контейнер, тогда вручную `docker compose up -d app`).
|
||||
@@ -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
|
||||
@@ -0,0 +1,116 @@
|
||||
# Чистый PDF — инструмент «URL → PDF» в LMS
|
||||
|
||||
Дата: 20260706
|
||||
Статус: дизайн утверждается
|
||||
Референс: Clean PDF (`pdf.brainysnipe.ru`) из поста https://t.me/flowing_abyss/94, инструкция https://flowing-abyss.com/Clean-PDF-for-Zotero
|
||||
|
||||
## Что делаем
|
||||
|
||||
Инструмент для студентов школы: превращает любую веб-страницу в чистый PDF — без рекламы, меню и мусора, с нормальной типографикой, оглавлением, форматами A4/Letter и светлой/тёмной темой. Два сценария плюс открытый API:
|
||||
|
||||
1. **Веб-форма** в кабинете (`/tools/clean-pdf`): вставил URL — скачал PDF.
|
||||
2. **Zotero**: персональный API-ключ + скрипт для плагина Actions & Tags — правый клик на записи → PDF генерируется и прикрепляется к ней.
|
||||
3. **Открытый API** для продвинутых: `curl` с Bearer-ключом, документация на странице инструмента.
|
||||
|
||||
## Принятые решения
|
||||
|
||||
| Вопрос | Решение |
|
||||
|---|---|
|
||||
| Архитектура | Всё внутри LMS (подход B): страница и API в Next.js, рендер — отдельный контейнер browserless в том же compose |
|
||||
| Кому доступ | Любой платный студент: есть `CourseEnrollment` на курс со slug ≠ `FREE_COURSE_SLUG`; admin/curator — без ограничений |
|
||||
| Видимость | Часть Obsidian Toolbox, живёт под существующим флагом `TOOLBOX_VISIBLE`; включение на проде — отдельное решение позже |
|
||||
| Лимит | 100 генераций/месяц на студента, env `PDF_MONTHLY_LIMIT` (без пересборки), плюс burst-лимит 5/мин |
|
||||
|
||||
## Пайплайн генерации
|
||||
|
||||
```
|
||||
URL студента
|
||||
→ Chromium (browserless) загружает страницу (JS отрабатывает)
|
||||
→ Next.js забирает итоговый HTML
|
||||
→ Defuddle (npm, через JSDOM) выделяет главный контент
|
||||
→ чистый HTML-шаблон: типографика, A4/Letter, light/dark, оглавление из заголовков
|
||||
→ Playwright page.pdf() в том же browserless
|
||||
→ файл application/pdf в ответ
|
||||
```
|
||||
|
||||
- **Defuddle** — библиотека kepano (автор Obsidian), та же, что у референса: https://github.com/kepano/defuddle
|
||||
- **browserless/chromium** — отдельный контейнер в docker-compose: `MAX_CONCURRENT_SESSIONS=2`, встроенная очередь, `mem_limit` ~1 ГБ, порт наружу не публикуется.
|
||||
- Next.js подключается по WebSocket через `playwright-core` (только клиент, Chromium в образ LMS не попадает — образ не растёт).
|
||||
- Таймаут всего пайплайна ~90 сек; понятные JSON-ошибки: URL недоступен, лимит исчерпан, нет платного доступа.
|
||||
|
||||
## Данные (Prisma)
|
||||
|
||||
Новая модель:
|
||||
|
||||
```prisma
|
||||
model PdfApiKey {
|
||||
id String @id @default(cuid())
|
||||
userId String @unique
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
key String @unique // формат sbpdf_<random>, показывается студенту
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
```
|
||||
|
||||
- Ключ создаётся лениво при первом заходе на страницу инструмента.
|
||||
- Перегенерация заменяет ключ (старый перестаёт работать сразу).
|
||||
- Учёт использований — существующая `ToolUsage` с `tool = "clean-pdf-generate"`; месячный счётчик = COUNT за календарный месяц. Именно отдельный идентификатор: `CopyButton` на странице логирует копирования как `tool = "clean-pdf"`, и они не должны тратить лимит генераций.
|
||||
- Проверка платного доступа выполняется на каждый запрос → рефанд или блокировка аккаунта автоматически отключает и веб, и ключ. Источник истины один — БД LMS.
|
||||
|
||||
## API
|
||||
|
||||
`GET /api/pdf?url=<...>&format=A4|Letter&theme=light|dark`
|
||||
|
||||
- **Авторизация, два пути**: заголовок `Authorization: Bearer sbpdf_...` (Zotero, curl) **или** активная сессия Better Auth (веб-форма — студенту ключ для веба не нужен).
|
||||
- Роут добавляется в `PUBLIC_ROUTES` в `middleware.ts` (иначе 307 → /login — известные грабли).
|
||||
- Ответ: `application/pdf` + заголовки `X-Uses-Count` / `X-Max-Uses` (их же показывает Zotero-скрипт).
|
||||
- Ошибки: JSON `{ error }` c честными статусами (401 нет ключа/сессии, 403 нет платного доступа, 429 лимит, 422 плохой URL, 504 не отрендерилось).
|
||||
|
||||
## Страница /tools/clean-pdf
|
||||
|
||||
По образцу существующих инструментов тулбокса (реестр `TOOLS`, `ToolCard`, страница + клиентская форма):
|
||||
|
||||
1. **Форма**: поле URL, выбор формата и темы, кнопка «Скачать PDF», счётчик «использовано N из 100 в этом месяце».
|
||||
2. **Блок «Zotero»**: персональный ключ (показать/скопировать/перегенерировать), пошаговая инструкция по Actions & Tags, готовый скрипт с уже подставленными адресом школы и ключом студента (адаптация скрипта референса: `SERVICE_URL = https://school.second-brain.ru`, endpoint `/api/pdf`).
|
||||
3. **Блок «API»**: пример curl.
|
||||
|
||||
Дизайн — строго ДС-2 (перед вёрсткой перечитать `SBT/02-Стандарты/Дизайн-LMS/DESIGN.md`).
|
||||
|
||||
## Безопасность
|
||||
|
||||
Сервис скачивает произвольные URL изнутри прод-стенда — главный риск SSRF:
|
||||
|
||||
- Только `http://` и `https://`.
|
||||
- Перед загрузкой — резолв DNS и блокировка приватных/зарезервированных диапазонов: localhost/127.x, 10.x, 172.16–31.x, 192.168.x, 169.254.x (метаданные облаков), ::1, fc00::/7, плюс docker-хостнеймы стенда (`db`, `app`, `browserless`).
|
||||
- Внутри browserless — перехват сетевых запросов страницы (`context.route`/`context.routeWebSocket`): блокирует буквальные приватные/зарезервированные IP и хосты `localhost`/`*.local`/`*.internal`. **Честно про предел этой защиты:** `assertPublicUrl` резолвит DNS **ровно один раз — для исходного URL**, а browser-level фильтр хостнеймы не резолвит вовсе. Значит без проверки уходит **любой новый хостнейм после первого перехода**: цель редиректа, субресурс, `fetch` из JS, `ws://`. DNS-rebind для обхода даже не нужен — достаточно редиректа на внутреннее имя. Остаточный риск закрывается только на сетевом уровне — см. «Деплой». *(уточнено 20260912 аудитом кода)*
|
||||
- Порт browserless наружу не публикуется, доступен только приложению по внутренней сети compose.
|
||||
- Куки/учётные данные пользователя на целевую страницу не передаются.
|
||||
- Санитайз имени файла в `Content-Disposition`.
|
||||
- Лимиты: 100/мес (env) + burst 5/мин на пользователя; сверху — очередь browserless (2 конкурентных рендера).
|
||||
- ⚠️ Follow-up (не блокирует релиз): оба счётчика лимита считают только **успешные** генерации (`ToolUsage` пишется после успешного рендера) — неудачные попытки (таймаут, 5xx с целевого сайта, зависший рендер) лимит не расходуют. Потенциальный ограниченный self-DoS повторными запросами к тяжёлым/неотвечающим URL. Рассмотреть подсчёт попыток, а не только успехов, отдельной задачей.
|
||||
|
||||
## Деплой
|
||||
|
||||
- `docker-compose.yml` (dev) и `docker-compose.prod.yml`: сервис `browserless` (образ `ghcr.io/browserless/chromium`), внутренняя сеть, лимиты памяти.
|
||||
- env: `BROWSER_WS_URL`, `PDF_MONTHLY_LIMIT=100`, существующий `TOOLBOX_VISIBLE`.
|
||||
- Prisma-миграция (`PdfApiKey`).
|
||||
- Стандартная схема деплоя LMS: сборка на Hetzner → `docker save | ssh | docker load` на Hoster.kz; browserless на Hoster.kz — обычный `docker pull`.
|
||||
- Hot-standby на Hetzner получает тот же compose (репликация БД уже покрывает `PdfApiKey` и `ToolUsage`).
|
||||
|
||||
> ⚠️ **Перед тем как поднять контейнер `browserless` на проде, обязательно захардить его сетевой egress** — заблокировать `169.254.0.0/16` (cloud-metadata) и RFC1918 (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`) на уровне хост-файрвола (`DOCKER-USER`) или выделенной internal-only docker-сети.
|
||||
>
|
||||
> **Гейт привязан к рендереру, а не к флагу** *(уточнено 20260912)*: `TOOLBOX_VISIBLE` прячет только страницы `/tools/*`, а роут `/api/pdf` лежит в `PUBLIC_ROUTES` и авторизуется сам — он станет рабочим для любого платного студента в момент появления `browserless` и `BROWSER_WS_URL`, ещё до поднятия флага. Именно это закрывает остаточный SSRF-риск из раздела «Безопасность»: browser-level фильтр его не закрывает, потому что не переразрешает хостнеймы.
|
||||
|
||||
## Тестирование
|
||||
|
||||
- Юнит: SSRF-валидатор (таблица адресов → допуск/блок), пайплайн Defuddle → HTML-шаблон на фикстурах.
|
||||
- Интеграция на staging: рендер 3–5 реальных статей (лонгрид, статья с картинками, JS-тяжёлая страница), проверка лимитов и обоих путей авторизации.
|
||||
- Zotero-скрипт — ручная проверка на живой библиотеке.
|
||||
- Веб-форма — e2e через agent-browser в `--headed` (headless режет антиспам — известные грабли).
|
||||
|
||||
## Вне рамок (YAGNI)
|
||||
|
||||
- Отдельный домен `pdf.second-brain.ru` — не нужен, всё на `school.second-brain.ru`.
|
||||
- Хранение сгенерированных PDF на сервере — файл отдаётся сразу и не сохраняется.
|
||||
- Пер-курсовый гейтинг, тарифные лимиты, платные квоты — не сейчас.
|
||||
- Освещение в рассылке/анонсы — после включения `TOOLBOX_VISIBLE` на проде, отдельной задачей.
|
||||
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 мин).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"}
|
||||
@@ -0,0 +1 @@
|
||||
/Users/dementiy/.local/share/uv/tools/graphifyy/bin/python3
|
||||
@@ -0,0 +1 @@
|
||||
/Users/dementiy/Documents/Claude/lms-system
|
||||
@@ -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._
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
@@ -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": []}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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": []}
|
||||
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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": []}
|
||||
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
@@ -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": []}
|
||||
Vendored
+1
@@ -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": []}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
@@ -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": []}
|
||||
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -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": []}
|
||||
Vendored
+1
@@ -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"}]}
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user