# «Мои материалы» — v2: харднинг и доводка > Составлен Opus 2026-06-13. Базовый план (PLAN.md, Фазы 1–6) **полностью реализован**. > Его раздел «Сквозные риски» отложил ровно то, что закрывает этот план: учёт/лимиты/чистку > хранилища и `materials.test.js`. Источник истины по текущему состоянию — код > (`studentMaterialsController.js`, `materials.js`, `my-materials.html`, `board-clip.js`, > `material-save.js`) и [[reference_student_materials]]. Готчи проекта: новый `:id`-роут → `// @public-by-design` + проверка владельца; большие HTML — только Edit; без эмодзи (inline SVG `.ic`); коммит поимённо + push; перезапуск сервера при правке backend; ветка `feature/sim-builder` в рабочем дереве — НЕ коммитить чужие правки, только свои файлы. --- ## Фаза 1 — Целостность и безопасность (backend, фундамент) ✅ цель этого захода 1. **Ссылочно-подсчётная чистка файлов.** `DELETE /:id` и смена `url` (аннотация) сейчас оставляют файл в `uploads/materials/` сиротой. `share` копирует `url` дословно → несколько строк ссылаются на ОДИН файл, поэтому `unlink` только когда на `url` не ссылается ни одна строка. Хелпер `releaseFileForUrl(url)` вызывается ПОСЛЕ delete/update. 2. **Allowlist схемы URL.** `create`/`update` принимали любой `url` → `link` со схемой `javascript:` рендерится как рабочий `` (раздача делает это вектором учитель→ученики). Хелпер `safeUrl`: только `http(s)://` или app-relative `/…` (не `//host`); иначе 400. 3. **Квота на пользователя.** Колонка `bytes` (мигр. 073), счёт `SUM(bytes)`/`COUNT(*)`. Лимит по числу материалов — в `create()`; лимит по байтам — в `uploadPersonalFile` (до приёма файла). Конфигурируемо через `MATERIALS_MAX_ITEMS` / `MATERIALS_MAX_BYTES` (для тестов — низкий потолок). 4. **`backend/tests/materials.test.js`** — CRUD, владелец (403/404), коллекции, share-копия + роль/owner, валидация URL, лимит числа, ссылочная чистка (прямой вызов хелпера на временном файле). ## Фаза 2 — Производительность ✅ (частично) - ✅ `GET /api/materials` отдаёт **обрезанный** `body` (первые 1000 симв.) + флаг `body_trunc`; полный текст — ленивый `GET /api/materials/:id` (`getOne`, owner-only). Клиент `ensureFullBody()` подгружает перед просмотром/правкой/флешкартой (иначе правка сохранила бы усечённый текст). - ⬜ Пагинация/keyset — отложено (клиент пока фильтрует в памяти; включить при росте объёмов). - ⬜ Серверные миниатюры `board/image` — отложено (нужна обработка картинок; пока `loading=lazy`). ## Фаза 3 — Доводка заложенных фич ✅ - ✅ UI тегов: ввод в модалках создания/правки + чипы на карточке (клик → фильтр) + пилюля активного фильтра. - ✅ Ссылка «открыть исходный урок» на карточке (`/my-lessons?session=`, есть `source_session_id`). - ✅ Цвет папки (палитра 8 пресетов, тинт иконки в рейле) + сортировка папок «Выше/Ниже» в модалке правки (нормализует `sort_order` к индексам). `safeColor` гейтит inline-style инъекцию (только hex). ## Фаза 4 — UX ✅ - ✅ Варианты сортировки (новые/старые/имя/тип) — селект в тулбаре. - ✅ Множественный выбор (чекбокс на карточке) + панель массовых действий (переместить/удалить, reuse per-item API). - ✅ Живое превью KaTeX в редакторе заметки (oninput → `mmPreview` → `mathHtml`). ### Статус Сделано и проверено: **Ф1 целиком** (16 backend-тестов), **Ф2/Ф3/Ф4 ✅** (headless-смоук `my-materials.html`: синтаксис + рендер карточек с deep-link/тегами/чекбоксом + фильтр по тегу + bulk-bar + тинт папки). Осталось ⬜ (инфра, отложено): пагинация/keyset списка, серверные миниатюры board/image. --- ## Порядок **Ф1 (этот заход) → Ф2 → Ф3 → Ф4.** Ф1 — серверный фундамент (риск-возврат, без него фронт-фичи множат мусор). Дальше преимущественно фронтенд `my-materials.html` + точечные ручки API.