Commit Graph
199 Commits
Author SHA1 Message Date
adminsandClaude Opus 5.5 42518b2137 Mark quiz-lead merge logic with decision D-BIZ-017
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 14:38:12 +05:00
adminsandClaude Opus 5.5 767a0d5a6e Fix quiz-lead wiping lists and attributes of existing subscribers
PUT /api/subscribers/{id} with lists=[quiz] replaced all lists and attribs:
a Press reader taking the quiz dropped out of Press, buyers lost course and
updates_until, status "enabled" lifted blocklisting. Verified on Listmonk 6.2.

Now: add to the quiz list via PUT /api/subscribers/lists (action add), write
attribs with current lists and status and preconfirm_subscriptions=false,
leave blocklisted subscribers untouched, keep first-touch utm_*.

Bot protection: constant-time secret check, quizzes faster than 20 s are
dropped silently, global hourly cap and per-email cap, strict email check
(the address goes into a Listmonk SQL filter), result email at most once a
day per address. Quiz v2 fields: quiz_type, quiz_date, quiz_answers,
quiz_utm, optional result email through LMS (send_result). v1 bot payload
keeps working.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 14:17:54 +05:00
adminsandClaude Opus 5.5 651a115ab7 Регистрация: utm с лендинга из cookie sb_utm, если в адресе меток нет
Кнопки лендингов ведут на /register без меток, поэтому utmCampaign не был
записан ни у одного пользователя. Caddy кладёт строку запроса с utm_* в cookie
sb_utm на .second-brain.ru; страница регистрации берёт метки из адреса, а если
их нет — из cookie. Значения: [A-Za-z0-9._-], до 100 символов.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:16:09 +05:00
adminsandClaude Opus 5.5 d8777a7a23 Кликабельные ссылки: история фидбека у куратора и цитаты в письмах
Куратор видел адрес в своём фидбеке мёртвым текстом — linkify там не было.
В письмах (фидбек, ответы на вопросы и комментарии) quote() только экранировал:
кликабельность зависела от почтовика. Теперь ссылки оформлены <a>.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 14:48:59 +05:00
adminsandClaude Opus 5.5 4792df274e access-check: точное сравнение почты (без ILIKE-шаблонов), заблокированным — нет, секрет за постоянное время
По фоновой проверке безопасности: mode insensitive превращался в ILIKE,
и % / _ в адресе давали «доступ есть» за чужую почту; banned-пользователь
считался с доступом; секрет сравнивался обычным ===. Почты в базе — в нижнем
регистре (0 из 1426 с заглавными), вход нормализуется.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 15:06:06 +05:00
adminsandClaude Opus 5.5 83f34ea679 Внутренний метод /api/internal/access-check: есть ли у почты доступ к курсу
Для бота выдачи SB Access (payment-router): старый покупатель Second Brain
Vault оставляет почту на /tg/link, ключ входа в бота выдаётся, только если
доступ к курсу действительно есть. Тот же общий секрет, что у grant;
ответ только да/нет, почта без учёта регистра, истёкший доступ не считается.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 15:03:24 +05:00
adminsandClaude Opus 5.5 0a5368577c баннер архетипа: кнопка «Демо-хранилище» → «Second Brain Vault»
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 13:55:12 +05:00
adminsandClaude Opus 5.5 e2311facf4 Vault: цена 2990 → 4990 ₽ с 20261001
На прод выкатывается таймером vault-price-4990 на Hoster.kz
(/root/price-switch-20261001/switch.py) в 00:00 МСК.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 16:10:06 +05:00
adminsandClaude Opus 5 8223cbb963 feat(комментарии): письмо ученику об ответе под уроком
Ответы персонала в ветке комментариев никуда не уведомляли: ученик видел
ответ, только если сам возвращался в урок. У тредов вопросов письмо есть,
у комментариев не было — из-за этого ответ под уроком 6.3 «Холст» висел
непрочитанным, пока его не продублировали руками.

- sendCommentReplyEmail: тот же шаблон ДС-2, что у ответов в тредах, с текстом
  ответа внутри письма и кнопкой на урок. Свой лимит превью 4000 символов:
  ввод разрешает 10 000, а порог вопросов (2000) резал бы развёрнутые разборы.
- commentReplyRecipient: правила «шлём / не шлём» вынесены чистой функцией и
  покрыты тестами — не шлём при выключенной настройке, не-staff, удалённом
  родителе, ответе самому себе и пустой почте.
- notifyStudentOnCommentReply: тумблер в админке рядом с уведомлением о фидбеке,
  по умолчанию включён.
- Удалён неподключённый дубль comment-actions.ts: компонент комментариев ходит
  в lib/actions/student-actions.ts, а в дубле жил свой лимит 2000 — правка в нём
  не давала эффекта.

Проверено: type-check и lint чистые (3 ошибки линтера — существующий долг в
quick-enroll-modal и kinescope-player), vitest 13/13, письмо отправлено боем
и доставлено (Resend, last_event=delivered).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 11:49:45 +05:00
adminsandClaude Opus 5 273cb2d1b4 feat(planner): вход в конструктор планнеров по подписанному токену
GET /api/planner/start проверяет сессию и доступ к курсу planner,
подписывает короткоживущий токен (HMAC-SHA256, 15 минут) и отправляет
в planner.second-brain.ru. Токен уходит во фрагменте, а не в строке
запроса: так он не попадает ни в логи, ни в Referer.

Алгоритм подписи — зеркало packages/web/src/auth.mjs в репозитории
планнера; совместимость проверена.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1UnMjwLLs22j7rymn9vUq
2026-09-14 10:19:52 +05:00
adminsandClaude Fable 5 03aaec76c2 Put the reply text inside question notification emails
The notification only said that the school had answered and linked into
the LMS, so a student had to log in to read two lines. Delivered mail
plus an unread thread was the common outcome — one such thread was a
pre-sale question that never converted.

The mail now carries the answer itself (capped at 2000 chars, with a
pointer to the thread when longer or when files are attached). Quoted
blocks are HTML-escaped, which they were not before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 13:41:33 +05:00
admins a92592ec63 Autolink bare URLs in lesson comments
Comments render CommonMark, which leaves a pasted https://… as dead text —
students share links constantly and nobody writes [text](url) by hand. Add
remark-gfm so plain URLs, www hosts and emails become links (and tables and
strikethrough come along), plus table styling so a wide table scrolls inside
itself instead of stretching the page.

Sanitizing is unchanged: javascript: is neither linked nor autolinked, raw
HTML is still dropped, and third-party images are still rendered as plain
links by the img component.
2026-08-16 11:55:48 +05:00
admins 8fc9708233 feat(ads): конверсия «регистрация» в Google Ads по gclid
Рекламу по обучающим запросам (obsidian как пользоваться, obsidian заметки)
ведём на бесплатный вход в школу, а не на продажу: порог входа нулевой,
а из 19 бесплатных регистраций 6 дошли до покупки (32%).

Чтобы Google видел, какие клики дают студентов:
- User.gclid — идентификатор клика рядом с уже собираемыми UTM;
- gclid прокидывается из URL через page → форму → API (той же цепочкой);
- src/lib/ads-conversion.ts — серверная отправка конверсии 7719084914
  (UPLOAD_CLICKS). Серверно, а не тегом: после регистрации сразу редирект
  в кабинет, тег на странице «спасибо» не сработает — грабля из разбора
  платежей 20260810.

Всё gated по env (GADS_*): нет кредов — тихо не отправляем, регистрацию
не роняем. Миграция идемпотентна: колонка заведена на проде вручную.
2026-08-13 11:25:26 +05:00
adminsandClaude Opus 5 20a25695bc Fix .md attachment upload in student questions
Windows reports an empty or generic MIME type for .md files, so the
type-first check rejected them with "Разрешены только jpg, png, pdf, md"
even though the extension was allowed — a client hit this while sending
us a vault note.

Extension is now the primary gate; MIME is validated only when the
browser actually sent a meaningful one. Generic types fall back to a
type derived from the extension so the file is served correctly later.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 10:27:14 +05:00
admins 005cd329b2 Allow image uploads in lesson comments
Add an image button to the comment toolbar backed by a new upload route
(images only, 5 MB, stored under comments/<userId>/), inserted as Markdown.

Only images served from our own storage are rendered: a third-party src would
be fetched by every reader of the lesson, exposing their address. Such links
are shown as plain links instead — this also defuses the external images a few
existing comments already carry.
2026-08-06 11:41:19 +05:00
admins 917253990b Drop react-markdown node prop from rendered comment links
The custom `a` renderer spread react-markdown's internal `node` prop onto
the anchor, leaking node="[object Object]" into the DOM on every comment link.
Destructure it out.
2026-07-31 19:48:31 +05:00
admins 2498d0f8da Enrich lesson comments: Markdown formatting, edit, higher limit
Add a formatting toolbar + safe Markdown rendering (react-markdown +
rehype-sanitize), raise the length limit 2000 -> 10000, and let authors
edit their own comments (new editComment action + editedAt column shown
as an 'изменено' badge).
2026-07-31 19:35:58 +05:00
adminsandClaude Fable 5 08e212365d Link homework review entry points to pending filter
Dashboard tiles and the curator CTA pointed to /curator/homework without
a status filter, so the default all-submissions list (newest first)
buried old PENDING items pages deep — 19 June submissions sat unseen
for 6 weeks while the top of the list showed freshly approved work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 10:27:03 +05:00
admins 92fd88964e Accept buyer phone in grant API and enrich existing users
payment-router now forwards name/phone from the order. Store phone on
create; for existing users fill name (when it is an email placeholder)
and phone (when empty) without overwriting meaningful data. Also stop
dropping the parsed phone column in CSV import.
2026-07-11 19:41:49 +05:00
admins 5d94949810 Redirect large/non-inline lesson files to CDN instead of proxying
ZIP/DOCX etc. download natively — no need to force attachment; and the
157 MB Second Brain Vault archive must stream from Caddy with Range,
not through the app. Proxy attachment stays for small inline types (PDF/images).
2026-07-09 11:35:45 +05:00
admins 98cc023709 Force lesson materials to download as attachment with readable filename
Direct CDN links opened PDFs inline (no Content-Disposition), which reads
as 'can't download' behind popup blockers / on mobile. New proxy route
streams the file with attachment + human name; lesson link points to it.
2026-07-08 19:28:26 +05:00
admins 118fa3961f Surface regenerate errors in clean-pdf Zotero section
regenerate() silently did nothing on {ok:false} or a rejected server
action, risking an unhandled promise rejection and leaving the student
staring at a stuck "меняем…" button with no feedback. Wrap the action
call in try/catch and show an inline error message on failure.
2026-07-06 13:45:12 +05:00
admins 1ccf994112 Strip active content from extracted HTML before PDF print
Defuddle-extracted article content is injected raw into the PDF HTML
and rendered by a real browser (browserless). As defense-in-depth
against a compromised or malicious source page, remove script/style/
iframe/object/embed elements and on* event-handler / javascript: href
attributes from the parsed DOM before serializing it into the template.
2026-07-06 13:44:41 +05:00
admins 02b16e311b Make Zotero script comment dynamic and assert valid JS
The generated script's setup comment hardcoded school.second-brain.ru
even though baseUrl is already interpolated elsewhere in the template,
so the comment would lie on any other host. Also add a regression test
that the generated script parses as valid JavaScript, to catch escaping
mistakes in the template literal.
2026-07-06 13:44:13 +05:00
admins fd40b1e394 Recompute Zotero script reactively on key regenerate 2026-07-06 13:13:19 +05:00
admins 0cbea10e1a Add clean-pdf tool page with web form and Zotero section 2026-07-06 12:49:06 +05:00
admins 668dd29397 Add regenerate action for clean-pdf API key 2026-07-06 12:44:29 +05:00
adminsandClaude Sonnet 5 9ad4d762d7 Wrap /api/pdf handler in try/catch for JSON error contract
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-06 12:39:01 +05:00
admins c50a295290 Add /api/pdf route with key/session auth and limits
Adds GET /api/pdf: resolves the caller via a Bearer sbpdf_ API key
(PdfApiKey table) or a Better Auth session cookie, then gates on paid
course access, burst/monthly usage limits, generates the PDF via
generateCleanPdf, records a ToolUsage row, and streams the file with
X-Uses-Count/X-Max-Uses headers. Errors map to 401/403/422/429/504
JSON responses. Whitelists /api/pdf in middleware PUBLIC_ROUTES so the
route can perform its own auth instead of being redirected to /login.
2026-07-06 12:24:23 +05:00
admins 0c56b0b809 Bound PDF print timeout and block WebSocket SSRF in clean-pdf 2026-07-06 12:21:24 +05:00
admins 89383fab1d Add PDF generation pipeline via browserless and Defuddle
Connects to browserless over CDP (playwright-core), extracts article
content with Defuddle/JSDOM, renders it through buildCleanHtml, and
prints a PDF on a second page. Adds an in-browser request filter as a
second line of SSRF defense against redirects to private/localhost
hosts, on top of assertPublicUrl's DNS check.

Integration test is gated by RUN_PDF_INTEGRATION=1 (describe.runIf) so
it is skipped in a normal npm run test and only runs against a real
browserless instance.
2026-07-06 12:09:28 +05:00
admins ef46cdbe29 Add paid-access and usage limit helpers for clean-pdf 2026-07-06 12:00:07 +05:00
admins b49680c118 Add PdfApiKey model and key storage helpers
Adds a Prisma model for per-student Clean PDF API keys plus a
hand-written migration (no local Postgres to run `migrate dev`
against). getOrCreatePdfKey/regenerateKey wrap the model with
lazy-creation and rotation logic on top of generatePdfKey().
2026-07-06 11:53:31 +05:00
admins 1d5b4f9251 Add API key generator and Zotero script builder 2026-07-06 11:46:31 +05:00
admins 6316c09993 Add PDF HTML template with typography, themes and TOC 2026-07-06 11:40:24 +05:00
admins 9a74339087 Block v4-mapped IPv6 literals in SSRF validator 2026-07-06 11:37:05 +05:00
admins 2c619be043 Add SSRF URL validator for clean-pdf 2026-07-06 11:28:11 +05:00
adminsandClaude Fable 5 980c4d621e Use bullet marker in welcome scheme cards
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 09:43:38 +05:00
adminsandClaude Fable 5 8c2bd42be3 Let welcome header wrap on narrow screens
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 09:37:56 +05:00
adminsandClaude Fable 5 fa7c25465c Add public DS-2 welcome page on root for unauthenticated visitors
Second-brain scheme (logo + six cards with rays) reworked from the old
platform's infographic. Root stays role-redirecting for signed-in users;
middleware opens "/" as an exact match only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 09:32:49 +05:00
adminsandClaude Fable 5 a6835567f1 Open checkout modal from locked course cards on dashboard
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 16:35:30 +05:00
adminsandClaude Fable 5 c2f84079ac Add DS-2 checkout modal for locked courses
Form POSTs directly to payment-router /pay/order with the student's
email prefilled, so the purchase auto-attaches to the existing account.
Payment methods are fetched from /pay/methods with a robokassa fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 16:35:30 +05:00
adminsandClaude Fable 5 f8f246ca9b Add store catalog config for dashboard upsell checkout
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 16:35:30 +05:00
adminsandClaude Opus 4.8 d2094c9541 Make URLs in homework answers and feedback clickable
Apply linkify() to student submission text (curator review page + student
cabinet: reviewed/approved/pending) and to curator feedback text, so pasted
links (e.g. Yandex.Disk) render as clickable anchors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 19:29:21 +05:00
adminsandClaude Opus 4.8 7c8e72cd94 Add email notification on new lesson comment
Notify admins/curators (or configured notificationEmails) when a student
leaves a comment under a lesson. New setting notifyOnComment (default on) +
toggle in admin settings. Fires only for student comments, not admin/curator
replies; comment text is HTML-escaped in the email.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 10:02:07 +05:00
adminsandClaude Opus 4.8 39e2102a18 Gate Obsidian Toolbox behind TOOLBOX_VISIBLE flag
Toolbox is still being refined; hide it from clients on prod while keeping it
usable on staging. Entry points (header link, dashboard card) render only when
TOOLBOX_VISIBLE=true; a tools/layout redirects /tools/* to /dashboard otherwise.
Routes and ToolUsage table are untouched (no destructive migration).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 12:15:42 +05:00
adminsandClaude Opus 4.8 3e2c70dd51 Fix global CSS wipe: inject head/body code via DOM, not <head dangerouslySetInnerHTML>
Rendering <head dangerouslySetInnerHTML={headCode}> in the App Router root
layout let the manual <head> overwrite the React-hoisted <link rel=stylesheet>
during hydration — all Tailwind styles dropped for real users once JS ran
(headless snapshots taken before hydration looked fine). headCode held the
Yandex.Metrika snippet. Replace with HtmlInjector client component that appends
the code through the DOM post-hydration, recreating <script> nodes so they run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 12:13:58 +05:00
adminsandClaude Opus 4.8 a239607360 Show homework badge by review status, not just feedback presence
Curator list labeled APPROVED-without-feedback submissions as 'Без ответа'
(badge keyed on feedbacks only). Key it on status: Принято / С отзывом /
На рассмотрении / Отклонено / Без ответа. Dashboards already count by status.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 10:23:35 +05:00
adminsandClaude Opus 4.8 6317dade9e Add Dataview and Bases generators (Toolbox Phase 2)
Two more inside-LMS generators under /tools, completing the 6-tool set:
- Dataview: DQL builder (TABLE/LIST/TASK/CALENDAR, FROM/WHERE/SORT/LIMIT),
  output wrapped in a dataview code block
- Bases: .base YAML builder (views, filters and:, order, sort, groupBy, limit);
  filter expressions emitted as single-quoted YAML scalars for safety

Syntax verified against official Obsidian docs (research workflow). Audit fix:
Bases sort entries use 'column:' (not 'property:') per documented working .base
files; 'property:' is kept only for groupBy. 14 new tests (47 total).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:04:09 +05:00
adminsandClaude Opus 4.8 1aa99b9def Add Toolbox promo card to student dashboard (Мои курсы)
Entry point into /tools alongside courses, per gate #3 decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:00:58 +05:00