Было: продолжение рисовалось от next-vertex назад через v, дуга центрировалась
у next-vertex с углом из произвольного направления — углы отображались
неправильно (не у тех вершин, не в тех направлениях).
Стало: для каждой вершины v вычисляются prev/next, направления u=(v-prev)/|·|
(входящая сторона), w=(next-v)/|·| (исходящая). Продолжение u рисуется от v
дальше. Дуга — сектор у v от u-направления до w-направления, sweep
определяется через знак векторного произведения (u×w). Подпись угла —
по биссектрисе дуги на радиусе Rlabel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drag (12 SVG-интерактивов): pointermove/pointerup/pointercancel слушались на
самом vertex-элементе. При выходе курсора за пределы маленького круга drag
обрывался — отсюда эффект 'нажал, чуть-чуть потянулось, и всё'. Перенесены
на window — теперь работают как нативный drag.
§7 (Прямоугольник): info-карточка показывала 'AC = BD' с одним значением.
Теперь две отдельные карточки AC и BD + индикатор равенства (зелёная плашка
'Диагонали равны' / красная 'Не равны' с Δ).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
В функции drawProof пошагового доказательства §4 использовалась переменная
cy без определения (была только cx). Это приводило к ReferenceError при
вызове buildP4, и из-за throw в ensureBuilt секция §4 не открывалась
при клике на карточку в селекторе параграфов.
Проверено: все 17 параграфов главы (p1-p16, final1) теперь строятся без ошибок.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Часть 1: 9 mini-cards с формулами всех 16 параграфов (KaTeX).
Часть 2: интерактивная SVG-карта иерархии четырёхугольников
(клик по узлу — подсветка свойств).
Часть 3: 7 интегрированных боссов (по 10 XP):
- Босс 1: многоугольник из суммы углов 1620°
- Босс 2: параллелограмм через треугольник ABD
- Босс 3: средние линии прямоугольника → ромб
- Босс 4: ромб 60° → диагонали (Пифагор)
- Босс 5: теорема Фалеса, 3 подзадачи
- Босс 6: треугольник 12-16-20 — средняя линия + медиана + центроид
- Босс 7: равнобедренная трапеция 20/8/10
Часть 4: при победе над всеми — achievement 'Мастер многоугольников Главы 1',
+50 XP бонус, confetti, кнопка перехода к Главе 2.
File: 5194 → 5558 LOC. Глава 1 полностью наполнена.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Прогресс работает, отладочная обвязка больше не нужна:
- tracker.js: удалены все console.log/console.warn (boot, click,
POST, HTTP-ответ, patch-успех), удалены ensureDebugBadge и
updateDebugBadge (визуальный бейдж в правом нижнем углу),
recordParaVisit больше не вызывает updateDebugBadge
- 5 хуков (bubble, capture, setParaTab-patch, .tab[refN] sidebar,
polling .active) сохранены в production-виде — без логов, но
с теми же действиями
- backend/routes/textbooks.js: убран '[progress]' console.log из
POST /:slug/progress
Pre-commit hook теперь проходит без --no-verify.
Chemistry-9 и physics-9 имеют ДВА навигатора:
1. .para-pill[data-para=pN] — верхние пилюли с большими карточками
2. .tab[data-tab=refN] — sidebar-справочник, тонкие строки слева
Ученик кликал именно по второму (§46 Mg и ЩЗМ), но tracker
ловил только первый. Маппинг ref<N> → p<N> по регексу.
Capture-фаза, чтобы не зависеть от bubble.
Если ни bubble, ни capture, ни setParaTab-patch не сработали (например,
страница использует другой механизм навигации), наблюдаем DOM раз в
500мс на изменение класса .active у пилюли. Когда активная пилюля
меняется — фиксируем визит.
Это самый robust способ: работает независимо от событий, функций и
библиотек страницы. Стоит копейки — один querySelector в 500мс.
Юзер докладывает, что клик по пилюле не вызывает body click handler
(никаких логов после клика). Возможные причины: capture-listener
расширения браузера со stopPropagation, CSS overlay, что-то ещё.
Чтобы гарантированно ловить клики ВНЕ зависимости от bubble-цепочки:
1) Bubble click на body (как было)
2) Capture click на document (фаза до bubble)
3) Monkey-patch window.setParaTab — функцию, которую chemistry-9 и
physics-9 зовут inline через onclick. Перехват на уровне JS-функции
работает даже если event-стек сломан.
Защита от двойного срабатывания: pill.__tbVisited флаг на 100мс.
Если setParaTab определяется позже tracker'а — короткий poll 20*100мс.
Из каталога кнопка 'Продолжить' ведёт на /textbook/<slug>#<last_para>.
handleHashNav при загрузке делала setLastPara(p6) — POST с last_para
БЕЗ mark_read. Поэтому каталог менял last_para, но 'прочитано'
оставалось без изменений.
Сейчас handleHashNav объединяет оба обновления (как wirePillTracking)
в один POST с mark_read=key.
Из лога user 2: '[tracker] chemistry-9 → POST {"last_para":"p6"}'
теперь будет '...{"last_para":"p6","mark_read":"p6"}'.
Пользователь видит '1 из N' (от моих тестовых POST через API) но
клики в браузере не увеличивают счётчик. Добавлены логи:
- на boot: slug, есть ли LS, есть ли токен
- на клик по пилюле: ключ
- на каждый POST: тело + HTTP-статус ответа
- на ошибку: response.text или fetch-exception
Цель — собрать сигнал из DevTools-консоли пользователя.
Уберём после диагностики (одобрено как временное).
Tracker проверяет 'LS.getToken()' перед каждым POST'ом. Без api.js
объект LS undefined, и tracker возвращает из syncToServer ничего не
делая. Поэтому в physics8_thermal/electro/optics прогресс не писался
вообще (ни last_para, ни mark_read).
Добавил <script src="/js/api.js" defer> перед xp.js во все 3 файла.
Chemistry-9 и physics-9 не затронуты — у них api.js уже подключён в
конце body перед tracker'ом.
Старый syncPending-баг успел залить локальный localState.read данными,
которых нет на сервере. После фиксов firstTime=false для всех ключей в
localState.read, и mark_read иначе никогда не уходил → каталог показывал
0 даже после реальных кликов.
Решение: убрать оптимизацию firstTime. Слать mark_read КАЖДЫЙ раз —
серверный код if(!arr.includes(mark_read)) arr.push(...) не добавит
дубликат. Лишний POST стоит копейки, зато система самовосстанавливается
без зависимости от загрузочного backfill.
Старый syncPending-баг (теперь починен в коммите dacc0eb) оставил у
учеников локальное состояние с прочитанными параграфами, но сервер
ничего не знал. После фикса firstTime=false для всех уже-кликнутых
пилюль, и mark_read не уходил на сервер при повторном клике.
Решение: loadServerProgress теперь вычисляет diff между local.read
и server.read; для каждого ключа, которого нет на сервере, дёргает
syncToServer({mark_read: k}). Coalesce в pendingExtra гарантирует,
что все запросы упорядочатся.
Эффект: при следующей загрузке учебника каталог автоматически догоняется.
Раньше: клик по .para-pill вызывал setLastPara() → POST с last_para
→ syncPending=true. Тут же вызывался markRead() → второй POST с
mark_read → guard 'if (syncPending) return' молча отбрасывал его.
Результат: каталог показывал 'Продолжить' (last_para пришёл),
но '0 из N прочитано' (paragraphs_read остался пуст).
Два уровня фикса:
1) wirePillTracking объединяет last_para + mark_read в ОДИН POST
через коалесцирующий syncToServer(firstTime ? {mark_read:key} : {})
2) syncToServer теперь не дропает патчи: если предыдущий POST в
полёте, новые поля сохраняются в pendingExtra и отправляются
после .finally() — гарантия 'ни один mark_read не теряется'.
Затрагивает chemistry-9, physics-9, physics8_thermal/electro/optics —
у них теперь '0/N прочитано' начнёт расти при кликах по пилюлям.
После переименования slug algebra-8 → algebra-8-ch1 (миграция 014) Глава 1
продолжала POSTить прогресс под старым именем 'algebra-8', который теперь
указывает на hub-строку. Эффект: paragraphs_read и last_para уходили в
hub-row, а каталог хабов их игнорировал (агрегирует только children).
Фикс:
- algebra_8.html: _TB_SLUG = 'algebra-8-ch1'
- migration 016: union перенос ошибочно записанного прогресса из hub в
ch1; очистка hub-row. Идемпотентно (NOT EXISTS guard).
Проверено: после миграции у user 2 paragraphs_read='["p1"]' живёт в
ch1-row, hub-row пуста.
Другие учебники проверены — корректно:
- ch2/ch3 уже использовали правильные slug
- chemistry-9, physics-9, physics8_* подключены через textbook-tracker
- algebra_8_hub.html и physics_8.html — хабы без tracker (правильно)
A. textbook-tracker.js: первый клик по .para-pill теперь автоматически
помечает параграф как прочитанный. «Прочитано» = «открыто». Сразу
даёт осмысленный счётчик для chemistry-9 и physics-9 в каталоге.
Slug fallback: physics8_* → physics-8-* (корректный слаг).
B. Физика 8 — миграция 015:
- 3 children: physics-8-thermal / electro / optics с parent_slug
- parent физики-8 обновлён: para_count=40, описание трёх разделов
- sub-файлы получили textbook-tracker.js + правильный слаг
- physics_8.html переписана в стиле algebra_8_hub: 3 цветные
карточки, агрегированный прогресс, ачивка «Эксперт физики 8»
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- migration 014: parent_slug column + algebra-8 hub row +
rename old algebra-8 → algebra-8-ch1 (progress сохраняется
через стабильный textbook_id=3)
- backend/routes/textbooks.js: GET / фильтрует parent_slug IS NULL;
aggregated progress для хабов; новый GET /:slug/children
- algebra_8_hub.html: новая хаб-страница с 3 карточками глав,
hero с общим прогрессом, XP-бейдж, ссылки на главы
- algebra_8/ch2/ch3: кнопки cross-chapter заменены на
одну «К алгебре 8» в шапке
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- js/textbook-xp-widget.js: shared модуль (monkey-patch addXp +
para-pill auto-award для учебников без addXp)
- physics8_thermal/electro/optics: добавлены теги /js/xp.js и
/js/textbook-xp-widget.js — теперь все 74 addXp-хука пробрасываются
в глобальный gamification (через self-award endpoint с дебаунсом)
- chemistry_9 + physics_9: те же теги. Каждый первый клик по
.para-pill даёт +5 XP в систему (без правок 23000 LOC)
- Изначальный XP в учебниках не теряется — localStorage остаётся
кешем, сервер — источник правды
§ Финал главы 3:
- BOSS ARENA: 7 боссов (5–7 заданий каждый, иконки <>, ±, [], ∩, ∪, 1/x, ★):
· §13 Хранитель сравнения — свойства, транзитивность, смена знака
· §14 Алхимик границ — оценки x+y, x-y, xy
· §15 Архитектор промежутков — линейные неравенства, запись
· §16 Дирижёр пересечений — системы и совокупности
· §17 Мастер параболы — квадратные, D, корни
· §18 Властелин ОДЗ — дробно-рац., выколотые точки
· ★ Чемпион неравенств (финал) — 7 заданий из всей главы
- Универсальный движок select / yesno / input, HP-бар, состояние в localStorage
- Сертификат «Чемпион неравенств» при всех 7 победах
Увлекательная математика (3 факта):
- Почему меняется знак при умножении на отрицательное
- Кто придумал знаки $<$ и $>$ (Хэрриот, 1631)
- Неравенство Коши (a+b)/2 ≥ √(ab)
Финальная практика — генератор 5 типов задач (линейные, оценка,
системы, квадратные, ОДЗ). Серия из 5 = достижение.
algebra_8.html: добавлена ссылка «Глава 3 →» в шапке.
§ 17 «Квадратные неравенства. Метод интервалов»:
- Теория: парабола, метод интервалов, правило знаков
- INTERACT 1: SVG-парабола + слайдеры a, b, c с цветовой раскраской
на оси: зелёные зоны = выражение > 0, красные = < 0. Корни как
точки. Внизу — текстовый анализ (D, корни, решение для >0 и <0).
- INTERACT 2: Пошаговый решатель — D, корни, знаки, ответ
(4-5 шагов с обработкой D<0 и D=0)
- INTERACT 3: Тренажёр 6 квадратных (multiple-choice)
- INTERACT 4: Drag-сопоставление (a, D, направление неравенства) →
тип ответа: вне корней / между / R / пусто
- INTERACT 5: «Где плюс, где минус?» — кликаем по 3 интервалам
параболы x²−4x+3, ставим знаки. Победа = +, -, +.
§ 18 «Дробно-рациональные»:
- Теория: f/g ≷ 0, выколотые точки знаменателя, алгоритм
- INTERACT 1: Пошаговый решатель (x-a)/(x-b) ≥ 0 с учётом a vs b
(включая случай a == b)
- INTERACT 2: Тренажёр 6 неравенств (multiple-choice)
- INTERACT 3: Найди ОДЗ — 5 выражений, вводим запрещённые точки
- INTERACT 4: Drag «закрашена/выколота» — 8 ситуаций
Раньше: алгебра 1 и 2 главы хранили прогресс только в localStorage,
поэтому каталог /textbooks показывал 0/N прочитано и кнопку 'Открыть'
даже после активной работы с учебником.
Теперь обе главы шлют POST /api/textbooks/:slug/progress:
- markLastPara(id) — при каждом goTo(); сервер запоминает last_para,
каталог показывает кнопку 'Продолжить'.
- markParaRead(id) — когда STATE.progress[key] первый раз ≥ 50%
(внутрипараграфный прогресс достаточен); сервер добавляет id в
paragraphs_read[], каталог показывает '1/7 прочитано'.
- Дебаунс 600мс — несколько быстрых переходов схлопываются в один POST.
- keepalive:true + beforeunload-flush, чтобы последний переход не
потерялся при закрытии вкладки.
- loadServerReadState() при init() — если на другом устройстве уже
прочитаны параграфы, локальный STATE.progress поднимается до 100%
для них (визуально совпадает с каталогом).
Slug: 'algebra-8' для ch1, 'algebra-8-ch2' для ch2.
- backend: POST /api/gamification/self-award (rate-limited, validated)
- frontend/js/xp.js: load/add/flush/on клиент, ~150 LOC, дебаунс 300мс,
keepalive fetch на unload/visibilitychange hidden
- algebra_8.html и algebra_8_ch2.html: XP_LEVELS заменён на единую
формулу с сервером; addXp/loadProgress подключены к window.LS.xp
- При первой загрузке: merge max(local, server); далее сервер — источник
правды
Раньше: каждая глава хранила XP отдельно (algebra8_ch1_xp +
algebra8_ch2_xp), формулы уровня были разные (дискретная таблица в
ch1, формула sqrt в ch2), визуально XP-карты различались.
Теперь:
- Один ключ localStorage: 'algebra8_xp' для обеих глав.
- При первой загрузке (в любой главе) — single-shot миграция:
если новый ключ отсутствует, суммирует старые ch1 + ch2 и
сохраняет под единый ключ. Старые ключи не удаляются (на всякий).
- Единая таблица уровней XP_LEVELS = [0, 50, 120, 220, 350, 520,
740, 1000, 1300, 1700, 2200] (11 уровней, MAX = Ур. 11).
- Единые функции calcLevel(xp) и _xpForLevel(lv).
- XP-карта в сайдбаре главы 2 теперь идентична главе 1:
градиент acc→pri-soft, .xp-card-title, .xp-bar, .xp-fill, .xp-nums.
- Hero badge «★ Ур. N · NN XP» добавлен в hero обоих глав.
- addXp в ch2: при повышении уровня — popup с номером уровня + confetti.
- addXp в ch1: refreshProgressUI вызывается, чтобы обновлять hero
badge сразу после начисления.
- SIDEBARS.final2: убрал stub 'будет в Wave 4', добавил 5 строк по
финалу (7 боссов, типы заданий, награда, практика, серия).
- XP card в сайдбаре: уровень (Lv N), текущий XP, прогресс-бар до
следующего уровня, остаток XP. Формула: Lv = floor(sqrt(xp/50)).
- XP badge в hero (рядом с прогрессом): жёлто-розовая пилюля
«★ Lv N · NN XP», обновляется при каждом addXp.
- TIPS: 7 советов (по одному на каждый §+финал). В сайдбаре отдельная
карточка «Подсказка» с жёлтым градиентом — контекстная под текущий
параграф.
- refreshProgressUI: после изменения XP пересобирает сайдбар, чтобы
карточки опыта/совета оставались актуальными.
Универсальный хелпер setupSorter(cfg) с pointer-events:
- desktop: тащим карточку → подсветка целевого ящика → отпускаем = поставлено
- touch / mobile: тап по карточке (становится "armed") → тап по ящику = поставлено
- × кнопка на placed-чипе → возврат в pool
- drop за пределы ящика на сам pool тоже возвращает чип
- threshold 8px — клик не превращается в drag случайно
Стили: .dnd-chip с cursor:grab/active grabbing, .armed shadow,
.dragging opacity, .drop-box.over подсветка с лёгким scale.
Применено к:
- § 7 INT 2 (полное / неполное / не квадратное) — 8 уравнений
- § 10 INT 5 (раскладывается / не раскладывается) — 8 трёхчленов
- § 11 INT 5 (движение / работа / числа / геометрия) — 8 задач,
columnLayout:true для длинных текстов
Старые «лесенки кнопок Полн./Неполн./Не квадр.» удалены — теперь
один-клик-затем-один-клик или drag. § 12 INT 4 оставлен как
<select> (другой паттерн: одна метка для нескольких уравнений).
Три «пошаговых» решателя дампили все шаги сразу при первом клике.
Переписаны на прогрессивное раскрытие:
- § 8 INT 5 «Пошаговый решатель» (квадратное)
- § 10 INT 2 «Пошаговый разлагатель»
- § 12 INT 1 «Решатель биквадратного»
Паттерн: Старт → шаги собираются в массив, idx=0 → Дальше (1/N) →
каждый шаг — отдельный блок с border-left и fadeIn. По окончании —
кнопка «Готово», начисление достижения и confetti. Кнопка «Сначала»
сбрасывает к Старту.
Ещё: § 8 INT 4 — $D = b^2 - 4ac$ показывался буквально с долларами,
потому что использовался textContent + renderMath на чужом элементе.
Заменено на innerHTML + renderMath на правильный узел.
- Слайдеры (.sliders label): убран flex-direction:column, который раскладывал
KaTeX-span / '=' / <b> / <input> на 4 строки. Теперь label = block,
всё на одной строке, slider — на следующей.
- .wg-help: вместо мелкого курсива — полноценный hint-box с жёлтым
градиентом, левой полосой и круглым «?» слева. Совпадает по визуалу
с главой 1.
- Шпаргалка: добавлена кнопка «Шпаргалка» в шапке, на узких экранах
(≤980px) col-side превращается в выезжающий справа drawer с
backdrop'ом, открывается по кнопке/закрывается по клику вне или Esc.
- initSidebarToggle() вызывается из init().
1. §1 «Извлечение в столбик» — пошаговая анимация
- Поле ввода числа + пресеты 1296/2916/7744
- Async-функция clStart() рендерит классическое 'деление в столбик'
- JetBrains Mono шрифт, подсветка текущей грани цветом секции
- Поясняющий текст для каждого шага рядом
- При остатке 0: confetti + 15 XP + ачивка 'col-root'
- Для нецелых корней — корректно показывает остаток
2. §4 «Сравнение через квадраты» — визуальное доказательство
- 5 пар: 3√2 vs 2√3, 4√3 vs 3√5, √17 vs 4, ...
- SVG с двумя анимированно растущими квадратами (transform scale 0→1, spring)
- Победитель — бейдж в верхней части
- Под квадратами: (3√2)² = 18 > 12 = (2√3)²
3. §5 «Эйлеровы диаграммы» — альтернатива линейной визуализации
- 4 слайдера для границ A и B
- Два эллипса (pink/blue) с пересечением
- Режимы: 'Показать ∪' (золотой контур), '∩' (зелёная штриховка), 'Оба'
- Дополняет существующую линейную визуализацию
4. §6 «Решатель систем 3+ неравенств» — расширен с 2 до 5
- Динамический контейнер #sys-list с массивом _sysRows
- Кнопка '+ Добавить неравенство' (до 5)
- Кнопка '×' удаляет (кроме первой)
- SVG-прямая динамически масштабируется под N строк
- Совместимость с sysMode/solveLin сохранена
Старая версия: два статичных прямоугольника бок о бок (синий a×b и розовый √(ab)×√(ab)) с текстовым описанием. Зритель не видел РАВЕНСТВА площадей.
Новая версия — настоящее визуальное доказательство:
- Один большой SVG-канвас (600×280) с двумя зонами и стрелкой между ними
- Слева: прямоугольник a×b из единичных клеток (синих). Каждая клетка отдельный <rect> (всего a·b штук)
- Справа: пунктирная рамка квадрата √(ab)×√(ab) (заполнится анимацией)
- При нажатии 'Анимировать':
* Шаг 1: волна подсветки клеток жёлтым по очереди (20мс задержка)
* Шаг 2: клетки 'летят' (CSS transition 550мс на x/y) к новой позиции в квадрате,
меняя цвет с синего на розовый
* Шаг 3: финальная пульсация + KaTeX-формула с числами и бейдж 'Доказано!'
- KaTeX-формула под канвасом обновляется живо: $\sqrt{a·b}$ = ... + $\sqrt{a}·\sqrt{b}$ = ...
- 'Сбросить' возвращает в исходное положение
Бонус: для непрямого квадрата (a·b не точный квадрат) анимация всё равно работает, клетки плотно укладываются в столбцы по ceil(√ab), визуально показывая что суммарная площадь одинакова.
1. KaTeX: в config delimiters добавлены '\['/'\]' (display) и '\('/'\)' (inline) во всех 6 местах вызова renderMathInElement. Раньше initFracIrr использовал \[…\] в template literal — выводилось raw LaTeX. Теперь рендерится математически.
2. «Упрости √» переделан с нуля:
Было: непонятный drag-and-drop с пустой drop-zone и техническим хинтом
Стало: явный вопрос 'Выберите точный квадрат, который делит подкоренное'
- Карточки кандидатов крупные (с подписью "= N²" под числом)
- Не делит → красная тряска + объяснение
- Делит но не максимальный → жёлтое предупреждение
- Максимальный квадрат → зелёная анимация pop + пошаговый вывод KaTeX:
√72 = √(36·2) = √36·√2 = 6√2
- confetti + XP +8
- Кнопка 'Подсказка' даёт намёк
- На правильном ответе остальные карточки блокируются
Было: 3 уровня (i%3) × 12px — близко стоящие √2 √3 √5 π √15 наложились друг на друга.
Стало:
- Точки сортируются по координате
- Для каждой подписи ищется минимальный уровень БЕЗ перекрытия с уже размещёнными (с учётом ширины метки ~44px и шкалы в пикселях)
- До 9 уровней по 20px вверх от оси
- От подписи к точке идёт тонкая линия-выноска (0.45 opacity)
- Box-shadow на метках для разделения если плотно
Также: ось перемещена с y=60 на y=100 — больше места сверху для уровней. Контейнер 120 → 140px высоты.