Приложение обкатывается на боевом контуре, поэтому интерфейс надо проверять на
живых данных — а живые данные лежат у настоящих водителей.
* «Вход для сотрудника» (/dev) — логин учётки CRM, поддержан второй шаг 2FA;
дальше поиск водителя по ФИО/телефону/id и просмотр его экранов.
* Плашка «Смотрю как …» над всеми экранами: интерфейс в этом режиме неотличим
от водительского, и чужой баланс легко принять за свой.
* Вкладка «Отладка» (/debug, водителю недоступна): смена водителя, driver_id,
режим, срок токена, адрес API, коммит и время сборки, журнал последних
40 ответивших запросов, сброс кэша сервис-воркера.
* Пополнение под просмотром идёт в учебный контур: экран /mock-pay/:orderId
выбирает, что «ответит» банк, а настоящий экран оплаты разбирает эти статусы
своим боевым кодом. Ни рубля, ни чека 54-ФЗ.
Заодно доезжает незакоммиченная работа прошлой сессии: дев-прокси в
vite.config.ts, его тест и разобранный .env.example.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- pairing-сессия хранит poll_secret и confirm_code (lib/pairing), опрос ушёл
на POST: pair_session уезжал в query-параметре и оседал в access-логах, а
это половина ключа к JWT. Вторая половина в deep-link не уходит;
- на экране ожидания видно число, которое обязан назвать бот. По чужой ссылке
человек его не увидит — подтверждать нечего, токен не выдаётся;
- онбординг больше не просит вернуться и запросить код: Telegram подтверждает
номер шарингом контакта, приложение забирает токен опросом;
- суммы с копейками. formatMoney округлял до рубля, и «к оплате 350 ₽»
расходилось и со списанием банка, и с чеком (комиссия почти всегда даёт
копейки). Клавиша разделителя, нарисованная на клавиатуре, наконец работает;
- категорию долга выбирает водитель. Хардкод «Долг аренда» отправлял платёж
в аренду даже тем, у кого её нет; список приходит с сервера и там же
проверяется;
- ошибки бэкенда показываются словами (минимум СБП, потолок суммы) вместо
молчаливого «не удалось», статусы истории переведены полностью, возврат
денег отличим от отказа.
Тесты: 33 зелёных, tsc чист.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Задача CRM 4439. Плитка в меню действий → поиск по госномеру → список ремонтов
машины (дата, госномер, модель, пробег, состав работ) → существующая карточка
ремонта. Вход в историю есть и прямо из карточки авто, там же блок последних
ремонтов.
Что стоило внимания:
- поиск ищет и по машинам, выведенным из парка (их ремонты никуда не делись,
и в осмотрах такая машина видна), но не по проданным — парк ими не занимается.
Текст пустого результата говорит ровно это: механик с опечаткой в номере
делает вывод по этой строке, и она обязана совпадать с поведением бэкенда;
- сбой сети отличим от «ничего не нашли», и под ошибкой не остаётся выдача
прошлого запроса — иначе можно тапнуть и уехать в историю чужой машины;
- «Назад» возвращает туда, откуда пришли (лента, история машины, карточка авто),
а не в ленту всего парка. Прямая ссылка при пустом стеке не запирает экран;
- форма правки больше не оставляет лишнюю запись в стеке: до этого «Назад» из
карточки ремонта после «Отмены» возвращал обратно в форму правки;
- история и блок в карточке авто запрашивают ленту без фото — превью там не
рисуется, а бэкенд ради него лазил в хранилище на каждую строку.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Экран успеха рисовался только при PAID. При любом другом финальном статусе
опрос останавливался, а человек навсегда оставался на «Ожидаем оплату…» со
спиннером — то есть два из трёх тестовых сценариев СБП глазами не проверить.
- ветки отказа (в том числе исчерпанных попыток), истёкшего срока и возврата
средств: у возврата свой текст, новая попытка там ни при чём;
- ошибка запроса больше не выглядит как честное ожидание: после серии неудач
подряд экран говорит, что не может проверить оплату, и даёт повторить.
Раньше 404, 500 и рестарт бэкенда были неотличимы от «ждём оплату»;
- плашка способа оплаты показывала «СБП» и при оплате картой;
- после перезагрузки страницы сумма бралась из пустого router state и
показывалась как 0 ₽ — лучше не показать ничего, чем показать неправду.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
window.location.href=deep_link navigated the whole PWA to t.me, destroying the
/tg-wait polling screen -> the authenticated token was never fetched. Prefetch
the pairing session and open Telegram via an <a href> (OS app-switch, PWA stays
mounted polling); add an Open-Telegram fallback link on the wait screen.
Co-Authored-By: claude-flow <ruv@ruv.net>
Backend omits channels without a configured username; strict zod schema
threw on missing 'max', surfacing as 'Не удалось отправить код' instead of
routing to the onboarding screen. Make both optional + regression test.
Co-Authored-By: claude-flow <ruv@ruv.net>
MechanicsField (чипы + пикер ростера) в Create/Edit; mechanic_ids в payload;
RepairDetail показывает работавших + долю 1/N. Ростер: GET /v1/mechanic/mechanics-roster.
Co-Authored-By: claude-flow <ruv@ruv.net>
The two repair forms genuinely diverged in UX (inline price-unlock + photos
vs modal override, no photos), so a single merged form would be worse. The
real duplication was the works-catalog picker modal — extracted into one
tested presentational component, removing ~75 duplicated lines.
Co-Authored-By: claude-flow <ruv@ruv.net>
Header 'Итого' (sum of loaded items) -> 'За этот месяц' (sum + count
from 1st VLAT to now) via new /v1/mechanic/repairs/stats/month endpoint.
List now grouped by local day with a dashed separator showing date
('Сегодня' / 'Вчера' / '26 мая') + day total + repair count.
Bundle: index-BKRX_okx.js
В каждой карточке ленты ремонтов справа от строки работ выводим сумму
работ (works_total из бэка, без запчастей). Шапка над лентой переехала:
слева «Итого: NNN ₽ (X ремонтов)», справа кнопка «Обновить» (раньше
была одна сверху-справа).
В CreateRepairPage поле «Цена применённая» хранило цифру через
price_applied: number. При удалении символов React сразу подставлял 0
и его нельзя было стереть — приходилось каретку перед нулём ставить
руками. Поменяли тип на number | "" в WorkRow и в onChange оставляем
пустую строку в стейте, пока юзер не ввёл новую цифру. На submit и в
сумму "" трактуется как 0 (Number("") === 0).
EditRepairPage не задет — там цена правится через модалку, цена там
уже жила как string (useState(String(...))) и пустое было допустимо.
При создании/редактировании ремонта в форме был отдельный «Пробег на
момент замены» внутри блока замены масла — auto-sync от шапочного с
soft-dirty флагом по У24. На практике это всё равно заставляло механика
вводить значение дважды, если шапочный пробег вводился вручную (StarLine
недоступен): момент включения галочки случался после ручного ввода,
soft-sync не успевал отработать или механик не понимал что поле
автозаполнилось.
Упростили: единственный пробег — «Пробег (км)» в шапке формы. На submit
он же подставляется в oil_change.mileage_at_change. Поле «Следующая через»
оставили — это интервал, а не значение пробега.
Бэкенд не трогаем — схема та же (mileage_at_change всё ещё в payload).
В CreateRepairPage queryPartSuggestions после await перезаписывал
partsDraft.name значением q из момента старта запроса. При быстром вводе
("К" → "Ке" → "Кеп" → "Кепу") поздно вернувшийся ответ для "К" откатывал
поле обратно на "К" — пользователи это видели как «текст частично сам
стирается». Делаем функциональный setState и роняем ответ если name уже
другой. EditRepairPage не задет (там корректный паттерн).
Юзер обнаружил, что при поиске работ (worksPicker в Create/EditRepairPage)
шапка с инпутом всё ещё уезжает под status-bar. Эти модалки и аналоги
(PhotoAnnotateStep, ZoneDetailView, InspectionEditor) используют
fixed inset-0 — моё прежнее правило для .fixed.top-0 их не покрывало.
Добавил отдельный селектор для .fixed.inset-0.bg-background[/\95];
bg-black/40·50·90 (диалоги, фото-вьюверы) намеренно не задеты.
В iOS PWA при viewport-fit=cover и apple-mobile-web-app-status-bar-style=
black-translucent статус-бар прозрачно накладывается на шапки страниц
(пользователи жаловались скринами с overlap «11:37» поверх «Ремонт #12»).
Body теперь имеет padding-top/bottom env(safe-area-inset-*) — fix initial-
рендер и нижних кнопок. Для sticky/fixed .top-0 хедеров отдельно overrid'им
top: env(safe-area-inset-top), т.к. sticky прилипает к top:0 viewport, а
не к содержимому body. Селектор узкий (.sticky.top-0 / .fixed.top-0) —
не задевает .fixed.inset-0 полноэкранные модалки.
Активация раздела «Ремонт» в Premium Мехник PWA: лента + создание ремонта,
фоновая сверка прав (У35), различающие сообщения при отказе входа (У36).
Backend для всех новых API в taxi-dashboard (отдельный commit eccaebe).
Тулится на `ENABLE_STO_MODULE=true` в backend env.
PR-4: Login + useMeSync.
- src/api/me.ts: тип Me расширен (is_admin, can_login_pwa, departments,
support — для У35/У36).
- src/hooks/useMeSync.ts: фоновая сверка `/me` раз в 5 мин (У35). При
403 NO_PWA_ACCESS — сброс токена, revocation info в sessionStorage.
IndexedDB-черновики не трогаются.
- src/pages/Login.tsx: после успешного login делает getMe() и при 403
показывает баннер «У вас нет доступа» с кликабельным TG/телефонным
контактом (У36). Баннер также рендерится при revocation после
фоновой сверки.
- src/App.tsx: useMeSync() подключён в корне.
PR-5: лента ремонтов.
- src/pages/ActionMenu.tsx: снят `comingSoon` с тайла «Ремонт».
- src/pages/repairs/RepairsFeedPage.tsx: лента с infinite scroll (через
IntersectionObserver), плейсхолдер «В парке пока нет ремонтов» (У41),
ellipsis-имена работ + плюрализация «+N работ» (У40).
- src/hooks/useRepairsFeed.ts: useInfiniteQuery + stale-while-revalidate
+ visibility refresh (У38).
- src/lib/timeFormat.ts: формат «Сегодня / Вчера / 23 мая» (У39).
- src/api/repairsFeed.ts: GET /api/v1/mechanic/repairs клиент.
PR-7: создание ремонта (online-only).
- src/api/repairsCreate.ts: searchCars, getCarContext, searchWorks,
suggestParts, presignPhoto + uploadPhotoToTmp (PUT с правильным
Content-Type), createRepair (POST с Idempotency-Key).
- src/pages/repairs/CarSelectPage.tsx: поиск машины с debounce 200мс.
- src/pages/repairs/CreateRepairPage.tsx: карточка создания ремонта с 5
inline-блоками (Шапка / Пробег / Фото / Работы / Запчасти / Замена
масла). UUID v4 как Idempotency-Key. Карандашик-разблокировка цены
с обязательным комментарием (У19). Мягкая подписка пробега масла
на шапочный (У24). Дефолт next_in_km=10000 (У25). Полноэкранная
модалка поиска работ.
Routes: /repairs, /repairs/new, /repairs/new/:carId.
Co-Authored-By: claude-flow <ruv@ruv.net>
Previously slotsForType excluded all general slots for return-type
inspections and left only Домкрат, on the assumption that the car is
already familiar and приёмка should focus on damages/equipment. Feedback
from mechanics: при приёмке всё равно нужно фиксировать общее состояние
машины, иначе споры с водителем теряют доказательную базу. Now все типы
осмотров показывают одинаковый набор слотов.
Co-Authored-By: claude-flow <ruv@ruv.net>