kungfuman-platform

Журнал архитектурных решений (DECISIONS)

Каждый раз, когда реализация отходит от docs/SPEC.md, фиксируй здесь: дата — что — почему. Через три месяца детали не вспомнятся.

Формат записи:

## YYYY-MM-DD — Краткий заголовок
**Контекст:** что происходило.
**Решение:** что сделали иначе, чем в спеке.
**Причина:** почему.
**Последствия:** что это меняет дальше.

2026-09-09 — Инициализация репозитория

Контекст: пустой репозиторий akmaltong/kungfuman, есть ТЗ v1.0. Решение: ТЗ сохранено как docs/SPEC.md; заведены CLAUDE.md, этот журнал и скелет структуры из раздела 4.5. Внутреннее имя проекта — kungfuman-platform. Причина: раздел 12 спеки прямо предписывает этот порядок. Последствия: дальнейшая работа идёт этапами из Части II, по одному за сессию.

2026-09-09 — Каркас Next.js собран вручную

Контекст: этап 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.

2026-09-09 — Разведение по ролям в middleware явными префиксами

Контекст: пути route-групп (student)/(staff) не видны в URL. Решение: в lib/supabase/middleware.ts логические зоны заданы явными списками префиксов (STAFF_PREFIXES, STUDENT_PREFIXES). Причина: middleware видит финальный URL, а не имя группы. Последствия: при добавлении новых разделов админки/кабинета не забывать дополнять эти списки.

2026-09-10 — Публичная программа читается service-role клиентом

Контекст: этап 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.

2026-09-10 — UI-примитивы вручную, сортировка кнопками (DnD отложен)

Контекст: этап 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).

2026-09-10 — Подпись видео и продающая страница курса

Контекст: этап 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.