Skip to content

Правки Tutor+ после теста 19–20.07.2026 - #54

Merged
Kengston merged 10 commits into
mainfrom
dev
Aug 10, 2026
Merged

Правки Tutor+ после теста 19–20.07.2026#54
Kengston merged 10 commits into
mainfrom
dev

Conversation

@Kengston

Copy link
Copy Markdown
Owner

Семь пунктов отчёта по правкам плюс три дефекта, найденных ручным прогоном.

Пункты отчёта

  1. «Ожидают оплаты» — заголовок больше не застревает на промежуточной сумме счётчика.
    2/3. «Отметить оплату» и «Отменить» в деталях урока открывают свои шторки.
  2. Имя профиля после входа; при пустом имени — плейсхолдер вместо прочерка.
  3. Стабильность: устранена невидимая шторка, перехватывавшая клики, и uncaught-ошибка подписки.
  4. Сессия переживает перезагрузку и deep-link; «назад» с прямой ссылки ведёт на «Сегодня».
  5. Уведомления приведены к эталону: 2 вкладки + чипы категорий (решение согласовано).

Корневая причина пунктов 1, 2, 3, 5

Кадровый цикл встаёт, когда вкладка или приложение в фоне (замерено 0 кадров за 10 с). Анимации, которые ВЕДУТ элемент из скрытого состояния в рабочее, замирали на полпути: шторка оставалась за экраном, а её полноэкранный скрим продолжал ловить клики. Вход шторки, счётчика и снека переведён на схему «анимация + страховочный таймер», где состояние покоя достижимо без единого кадра.

Дополнительно найдено ручным прогоном

  • Снек жил на opacity 0 и молча исчезал — вместе с ним была недоступна кнопка «Вернуть».
  • Строка чипов в уведомлениях сплющивалась до 18 px на длинной ленте.
  • Подпись «следующее в HH:MM» вылезала за карточку у крайних точек ленты дня.

Плюс защита от потери регулярного расписания: отказ операции по защищённому деньгами уроку больше ничего не меняет в слотах.

Проверки

tsc, expo lint, vitest (119 тестов), expo export — зелёные. Ручной прогон проведён на задеплоенном превью и локально.

Остаётся посмотреть глазами на устройстве: движение анимаций (в тестовой среде кадров нет).

Kengston added 10 commits August 2, 2026 13:38
Фоновая вкладка глушит requestAnimationFrame, и анимации, которые ВЕДУТ элемент
из скрытого состояния в рабочее, замирали на полпути:
- Sheet: entering={SlideInDown} оставлял панель под вьюпортом (translateY 613px,
  скрим 0.069) — шторка «не открывалась», а её полноэкранный скрим продолжал
  глотать клики, из-за чего приложение выглядело зависшим;
- CountUp: заголовок «Ожидают оплаты» застревал на 548/630 вместо 6 000.

Вход шторки переведён на обычный shared value с дедлайн-таймером: таймеры тикают
и без кадров, поэтому незавершённый вход роняет панель в статичную позицию покоя.
Счётчик получил такую же страховку до финального значения.

Тест на согласованность: заголовок = Σ бакетов «По сроку» = Σ списка должников.
Экран уведомлений приведён к эталонному прототипу: вкладки read-состояния теперь
«Все / Непрочитанные», а тип события выбирается строкой чипов под ними
(«Все · Уроки · Оплата · Расписание · Система») вместо спрятанной за воронкой
модалки. «Прочитать всё» и иконка «Настройки» в шапке сохранены — они не спорят
с моделью эталона и полезны на длинной ленте.

Вкладка «Прочитанные» = «Все» минус «Непрочитанные», отдельной вкладки не нужно.
Ключи notif.tab.read / notif.filterType / a11y.notifFilter удалены вместе с
модальным фильтром.
Сессия хранилась только в памяти, поэтому F5 на внутреннем экране и прямой переход
по ссылке выбрасывали на /sign-in. Флаг переехал в единственную строку profiles
(schema v12, signed_in, nullable → null читается как «не вошёл»), ProfileGate
поднимает его ДО монтирования дерева и отдаёт в AuthProvider — гейт больше не видит
ложного кадра «не авторизован» и не уводит с запрошенного маршрута.

Следом лечится и «назад» после deep-link: стек начинается сразу с детального экрана,
router.back() там отвечает «The action GO_BACK was not handled» и оставляет юзера на
месте. Общий useBack()/backOrHome() уходит на корень, когда возвращаться некуда.

Ещё из наблюдений теста:
- useStudent('') на первом рендере деталей урока ронял необработанную ошибку
  «Record students# not found» — пустой id больше не подписывается, а ошибки
  подписки логируются вместо вылета наружу;
- пустое имя профиля в настройках показывалось прочерком — теперь «Добавьте имя».
Занятие, по которому есть операция с деньгами, защищено от групповых правок
(ADR-0016 §4) — scopeCancel/scopeReschedule для него не трогают ни одной записи.
Экран при этом показывал «Урок отменён» и уводил назад: пользователь видел успех,
а после возврата урок оказывался на месте.

Теперь оба пути смотрят на affected: ноль — честный снек «По уроку есть оплата —
изменение не применено» и остаёмся на экране; иначе прежний снек с «Вернуть» и
возврат в расписание.
…причину

По итогам ревью — два блокера.

1. Спасательный таймер шторки армился только после onLayout, а onLayout в
   react-native-web едет на ResizeObserver, который доставляется в том же шаге
   рендеринга, что и requestAnimationFrame. В фоновой вкладке высота панели не
   приходит никогда — таймер не взводился, панель оставалась с opacity 0, а
   полноэкранный скрим по-прежнему собирал клики. Теперь высота — только уточнение
   дистанции (до неё FALLBACK_TRAVEL), а окно входа закрывает таймер, взводимый
   безусловно; после него панель переходит на drag-only стиль, который встаёт в
   позицию покоя без единого кадра.

2. affected === 0 трактовалось как «есть оплата», хотя домен отказывает ещё и по
   проведённому/отменённому занятию и по пустому окну охвата. Причина теперь
   выбирается по факту, а «Отменить» на уже отменённом уроке выключена.
…артует по поздней вёрстке

Адверсариальное ревью ветки (6 дименшенов + рефьютеры) подтвердило 11 находок.

Критично — src/db/scope.ts: при affected === 0 запись слота выполнялась всё равно.
scopeCancel закрывал серию (activeTo = watershed), scopeReschedule переписывал время
всей будущей серии (timeMin) — при том что вызывающий код показывает «изменение не
применено» и в отказе НЕ предлагает «Вернуть». Пользователь терял регулярное расписание
молча и необратимо: materializeSlots такой слот больше не берёт, а из экрана расписания
он исчезает, то есть переоткрыть его через UI нельзя. Обе операции теперь выходят
no-op'ом до записи.

Sheet: измеренная высота панели переехала из состояния в shared value. Она стояла в deps
эффекта, и её позднее прибытие (ResizeObserver голодает вместе с rAF) сносило уже
спасённую таймером шторку — панель уезжала под вьюпорт и переигрывала вход, а невидимый
скрим эти ~300 мс собирал тапы. Заодно исправлено объяснение механизма спасения в шапке:
работает не «пересчёт стиля на каждом рендере», а смена идентичности стиля.

CountUp: отложенный кадр после settle-таймера сбрасывал заголовок в 0 и пересчитывал —
теперь гасится флагом.

Мелочи: мёртвый ключ common.apply, устаревший комментарий про модальный фильтр.
Найдено ручным прогоном.

Snackbar лежал в том же классе, что Sheet и CountUp, но не попал в первую правку:
бар стартует прозрачным и на 14px ниже, а поднимала его связка onLayout + withTiming —
обе кадрозависимые. Замер при нуле кадров: снек монтируется с верным текстом, живёт весь
свой срок на opacity 0 и молча исчезает через 3.5 с. То есть подтверждения действия не
видно вообще, а вместе с баром недоступно и «Вернуть» — единственный путь отката отмены
урока и записи оплаты. Вход перенесён в эффект (ему кадры не нужны) и подстрахован
дедлайн-таймером, как в Sheet.

Строка чипов категорий в уведомлениях — регресс правки п. 7: ScrollView остался
сжимаемым flex-ребёнком, и на достаточно длинной ленте его сплющивало до 18px, обрезая
подписи чипов посередине. Зафиксирован flexShrink: 0.
Правки после теста 19–20.07.2026 — все 7 пунктов
Найдено ручным прогоном. Подпись фиксированной ширины 120 центрируется под точкой
в слоте шириной 20, то есть свисает на 50 px в каждую сторону. Для средних точек это
и задумано, но крайние точки прижаты к краям карточки, и там свис уходил наружу:
замер в первом слоте давал левый край текста на −16 при границе карточки 33, в
последнем — правый край на 202 при границе 153. На узком экране подпись срезало.

Случай не краевой: следующий урок оказывается первой точкой каждое утро, пока не
проведён ни один урок дня.

Теперь у крайних точек свис разрешён только ВНУТРЬ — текст встаёт ровно по границе
(33 и 153 в том же замере). Середина ленты не меняется.
Подпись «следующее в HH:MM» не вылезает за карточку
@Kengston
Kengston merged commit 6235b6e into main Aug 10, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant