Каждый раз, когда реализация отходит от docs/SPEC.md, фиксируй здесь: дата — что —
почему. Через три месяца детали не вспомнятся.
Формат записи:
## YYYY-MM-DD — Краткий заголовок
**Контекст:** что происходило.
**Решение:** что сделали иначе, чем в спеке.
**Причина:** почему.
**Последствия:** что это меняет дальше.
Контекст: пустой репозиторий akmaltong/kungfuman, есть ТЗ v1.0.
Решение: ТЗ сохранено как docs/SPEC.md; заведены CLAUDE.md, этот журнал и
скелет структуры из раздела 4.5. Внутреннее имя проекта — kungfuman-platform.
Причина: раздел 12 спеки прямо предписывает этот порядок.
Последствия: дальнейшая работа идёт этапами из Части II, по одному за сессию.
Контекст: этап 1, задача 1. Спека предлагает create-next-app.
Решение: каркас написан файлами вручную (App Router, route-группы
(public)/(student)/(staff), клиенты Supabase, middleware, i18n, домен-стабы),
без интерактивного create-next-app.
Причина: воспроизводимость и ревьюабельность; интерактивный генератор в
CI/сессии неудобен.
Последствия: зависимости фиксированы в package.json. React закреплён на
19 RC (совместимая с Next 15.0.3 пара). lib/types/database.ts — плейсхолдер до
первой генерации pnpm db:types из локального Supabase.
Контекст: пути route-групп (student)/(staff) не видны в URL.
Решение: в lib/supabase/middleware.ts логические зоны заданы явными
списками префиксов (STAFF_PREFIXES, STUDENT_PREFIXES).
Причина: middleware видит финальный URL, а не имя группы.
Последствия: при добавлении новых разделов админки/кабинета не забывать
дополнять эти списки.
Контекст: этап 2, публичная страница программы должна читаться без
регистрации. Но RLS disciplines_read пускает анонима только к общешкольным
(school_id is null) дисциплинам, а у школы Худжанд дисциплины со school_id.
Скоупить по школе в RLS для анонима нельзя — у него нет школьного контекста.
Решение: публичный каталог (lib/queries/curriculum.ts) читается на сервере
service-role клиентом (lib/supabase/admin.ts), но строго ограничен: активные
дисциплины конкретной школы (по slug из URL), их уровни и только практики со
status = 'published'. RLS-миграции не менялись.
Причина: не ослаблять RLS ради публичной витрины; сервер сам сужает выборку.
Последствия: service-role ключ только на сервере, никогда в браузер. Альтернатива
на будущее — отдельные публичные SELECT-политики, если появится анонимный
школьный контекст. Каст к дефолтной схеме в запросе убрать после pnpm db:types.
Контекст: этап 2, редактор методики. Спека: shadcn/ui и drag-and-drop
сортировка практик.
Решение: shadcn CLI не запускался (тянет внешний реестр в сессии) — сделаны
минимальные components/ui/{button,input}.tsx в айдентике. Переупорядочивание
практик — кнопками ↑/↓ через server action (movePractice + домен moveItem),
без клиентского JS и без DnD-библиотеки.
Причина: редактор работает целиком на server actions (прогрессивное
улучшение, ноль клиентского состояния), это надёжнее для одного владельца-админа.
Последствия: true drag-and-drop (@dnd-kit) — отдельная задача-полировка.
Реальное наполнение уровней Цигун содержанием делает Акмал через этот редактор
(инвентировать методику нельзя — это главный актив, принцип №1).
Контекст: этап 6. Спека: подписанные ссылки на видео (никогда не отдавать
прямой URL), продающая страница с полной программой и превью.
Решение: /api/media/sign отдаёт адрес воспроизведения, проверяя доступ
через RLS (урок читается → превью/куплено/персонал). Пока для Kinescope
возвращается embed-адрес — РЕАЛЬНАЯ генерация подписанного токена через их API
(KINESCOPE_API_TOKEN) добавляется при подключении аккаунта. Полную программу
курса (включая закрытые уроки — только заголовки) продающая страница читает
admin-клиентом (lib/queries/courses.ts), как и публичную программу; RLS не
ослаблялся. Сертификат — печатная HTML-страница (print → PDF), выдаётся только
при 100% прохождения; серверная генерация PDF (pdf-lib) — при необходимости.
Причина: не блокировать этап отсутствием живого аккаунта Kinescope; вынести
внешнюю интеграцию в одну функцию playbackUrl.
Последствия: заменить playbackUrl на реальную подпись; embed-хост
Kinescope/YouTube должен быть разрешён CSP при появлении заголовков.
Контекст: этап 7. Спека: роутинг /[locale]/....
Решение: локаль определяется getLocale() (cookie locale → Accept-Language
→ ru) с переключателем; путь не содержит сегмента локали. Вторая школа (Москва)
добавлена в сид для проверки изоляции тенантов. Вебхуки оплаты
(/api/webhooks/[provider]) принимают yookassa/paddle/alif и активируют
абонемент; проверка подписи — пока опциональный общий секрет WEBHOOK_SECRET.
Причина: path-роутинг /[locale] — рефактор всего дерева app/; выгоду для
SEO он даёт, но цена высока, а мультиязычный UX уже работает через cookie.
Реальная верификация вебхука требует секретов провайдера.
Последствия: для SEO-мультиязычности позже ввести app/[locale]/ и
hreflang. В вебхуке заменить общий секрет на подпись провайдера
(ЮKassa/Paddle). Проверку изоляции второй школы прогнать вручную после Supabase.