Commit Graph
138 Commits
Author SHA1 Message Date
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 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 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 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 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
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
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 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 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
adminsandClaude Opus 4.8 b6d555ab3b Add Obsidian Toolbox — 4 syntax generators under /tools
Inside-LMS toolbox for registered students: callout-CSS, YAML-frontmatter,
theme CSS-variables, Style Settings generators. Auth inherited from (student)
route group; no public surface, no email gate.

- Pure generators in src/lib/tools/* with Vitest (33 tests)
- Shared YAML-safe scalar quoter (_shared/yaml.ts) used by frontmatter +
  style-settings: unquoted user input was silently breaking YAML (tags '#x'
  -> null, color default '#7C3AED' -> null, 'a: b' -> parse error)
- hex validation in callout (no NaN), date input constrained, clipboard +
  analytics calls guarded
- Prisma ToolUsage model + migration; logToolUsage Server Action (auth-first,
  10-min dedup window per user/tool)
- Tool index /tools + ToolCard (explicit lucide map, no import *) + header link

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:45:52 +05:00
adminsandClaude Opus 4.8 f8aa27533c Add vault graph poster to Obsidian Wrapped
- parse.worker collects edges, builds top-150-by-degree graph (links capped 600)
- graphPoster.ts renders force-graph on off-screen canvas -> PNG (DS-2 colors),
  node names never leave the browser
- WrappedClient: Карточка/Граф toggle, optional node labels, lazy-load force-graph
- THIRD-PARTY-NOTICES for force-graph (MIT) and next/og satori/resvg (MPL-2.0)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:57:36 +05:00
adminsandClaude Opus 4.8 0a1425301d Add Wrapped link to student nav
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:25:48 +05:00
adminsandClaude Opus 4.8 06b8d9f64b Add Obsidian Wrapped feature (inside-LMS, client-side vault parsing)
- WrappedRun Prisma model + migration (aggregates only, no note names/text)
- /wrapped authed route: FSA + webkitdirectory + drag-drop reader, Web Worker parser
- archetype + score heuristics, ДС-2 result card
- POST /api/wrapped/run (userId from session), public /share/wrapped/[token]
- next/og OG image (Satori+resvg-wasm, alpine-safe) + Fira Mono ttf
- middleware: /share/wrapped public

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:19:59 +05:00
adminsandClaude Opus 4.8 0e67a53122 Add Яндекс.Метрика funnel goals: signup_free + gate_view (env-driven counter)
- window.ym reachGoal 'signup_free' on registration success
- GateViewTracker client component fires 'gate_view' on tripwire gate mount
- counter id via NEXT_PUBLIC_METRIKA_COUNTER_ID (build-time)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:13:32 +05:00
adminsandClaude Opus 4.8 7bf3935c81 Add freemium variant C: archetype persistence, dashboard banner, lesson gate
- User.archetype/utmSource/utmMedium/utmCampaign fields + migration
- register flow reads ?archetype/?utm_* and saves them on the User row
- dashboard ArchetypeBanner: per-archetype 'твой путь' block
- lesson gate after wow-moment lesson (WOW_MOMENT_LESSON_ID=obs-start-l2-006) → tripwire + full-course offer

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:26:34 +05:00
adminsandClaude Sonnet 4.6 bb6c806cdd Add quiz-lead internal endpoint for Typebot → Listmonk funnel
POST /api/internal/quiz-lead accepts archetype quiz results and
idempotently upserts subscriber in Listmonk (create on 201, PUT on 409).
Documents QUIZ_LEAD_SECRET and LISTMONK_* vars in .env.example.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 14:34:39 +05:00
adminsandClaude Opus 4.8 9fbb7aea6c Accept CDN URLs for question attachments
Attachments uploaded via question-upload get a CDN URL
(files.second-brain.ru via S3_CDN_URL), but the validator only accepted
the direct S3 endpoint prefix — so every attachment was silently dropped
(message text saved, file lost). Add isAllowedPublicUrl (CDN + direct S3)
in lib/s3 and use it in both question routes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 15:11:36 +05:00
adminsandClaude Opus 4.8 2436f77de5 Add 'approve without feedback' button for homework review
Curators/admins can mark a submission APPROVED (and complete the lesson)
without writing feedback. The student sees 'ДЗ принято ✓' and can no
longer resubmit. No email is sent on silent approval.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:10:29 +05:00
adminsandClaude Opus 4.8 a8cd2cd4d4 feat: auto-enroll new signups into free course obsidian-start (freemium)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:05:52 +05:00
adminsandClaude Opus 4.8 2ea9c6fda0 Don't force password change while admin is impersonating
The mustChangePassword guard redirected an impersonating admin to
/change-password, which sits outside the student layout (no
return-to-admin banner) — a dead end. Skip the guard when impersonating,
and add the stop-impersonate banner to the change-password page as a
fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:03:58 +05:00
adminsandClaude Opus 4.8 ec011ab7fd Add support mailto on login for missing password-reset email
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 12:58:28 +05:00
adminsandClaude Opus 4.8 04e8377dab Show hint on lessons with pending homework or quiz
Lessons with homework/quiz have no manual complete button — they
auto-complete on submission. Before submission nothing was shown,
leaving students unsure how to progress. Add a muted hint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 12:22:44 +05:00
adminsandClaude Opus 4.8 5d4630519c Force download for lesson materials (Content-Disposition: attachment)
Lesson files were served inline (no Content-Disposition) → browsers
opened PDFs in a new tab instead of downloading; flaky across
browsers/providers. Set Content-Disposition: attachment with the
human-readable filename (RFC 5987 for Cyrillic) on upload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:06:51 +05:00
adminsandClaude Opus 4.8 a7abf454d9 Add internal grant endpoint + temp-password / force-change flow
- /api/internal/grant: secret-auth endpoint for payment-router to
  provision access on a paid order — find-or-create user (emailVerified,
  temp password, mustChangePassword), enroll by course slug list,
  AccessLog, delivery email. Idempotent by order_id.
- User.mustChangePassword flag (+ migration); (student) layout redirects
  flagged users to /change-password (forced first-login change).
- email.ts: sendCourseGrantEmail (temp password for new buyers).
- middleware: /api/internal is public (own secret auth).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 16:25:35 +05:00
adminsandClaude Opus 4.8 85cf8be705 Explicitly resend verification email on unverified login
Better Auth does not auto-resend on 403 (verified against Resend logs),
so request a fresh link via authClient.sendVerificationEmail before
telling the user a new email was sent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 14:05:32 +05:00
adminsandClaude Opus 4.8 2790a7d24b Wire up email verification flow for new signups
requireEmailVerification was enabled but no sendVerificationEmail was
configured — new users saw "check your email", never received anything,
and couldn't log in (403 shown as "wrong email or password").

- email.ts: add verification email template; welcome email no longer
  claims the account is "confirmed"
- auth.ts: configure emailVerification (send on signup, resend on
  unverified login attempt, auto sign-in after verification, 24h TTL)
- register route: callbackURL=/dashboard so the verify link lands in
  the cabinet
- login form: distinguish 403 (unverified — tell user a fresh link was
  sent) and 429 (rate limit) from wrong credentials

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 14:01:46 +05:00
adminsandClaude Opus 4.8 8240db5d22 Make registration errors informative
- Backend: explicitly return 409 EMAIL_TAKEN when the email already
  exists, instead of Better Auth's silent 200 (common after migration).
- Frontend: split error handling — network failure, non-JSON body
  (502/proxy), and meaningful server errors get distinct messages; the
  "email already registered" case shows login / reset-password links
  instead of a generic "connection error".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:20:56 +05:00
adminsandClaude Opus 4.8 05f46ec830 Add "login as user" to user profile card
Extract ImpersonateButton into a shared component and reuse it on the
admin user profile page (header, hidden for admins) — same behaviour as
the users list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 15:55:57 +05:00
adminsandClaude Opus 4.8 a290b9495c Enroll modal: days input instead of date; hide bundle if base course owned
- Quick-enroll modal now takes access duration in days (0/empty = unlimited)
  and shows the computed expiry date, instead of a manual date picker.
- Dashboard upsell no longer shows the "Всё включено" bundle of a course
  the student already owns in its base edition (obsidian/zotero).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:13:15 +05:00
adminsandClaude Opus 4.8 3ed5540f40 Reduce blur on locked course cards for readability
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:06:32 +05:00