Передача 10.08.2026 — интерфейс для менеджеров, скрытие старого, разбор потери данных
Это вход для новой сессии. Прочитать целиком до первой правки. Постоянный вход в проект — ../../AGENTS.md и ../../for-ai-agents/START-HERE.md.
Предыдущее состояние: HANDOFF-2026-08-09.md — разворот JSON-блобов приёма в колонки и предел сторожа покрытия.
1. ⚠️ ПЕРВЫМ ДЕЛОМ: старый интерфейс СКРЫТ — как вернуть
Решение владельца 09.08.2026: для менеджера старого интерфейса больше нет. Сам он жив и не удалён, скрыта только публичная дорога к нему.
Что сделано. Правка одного файла на s2 — /data/hypertime/app/docker/nginx/conf.d/50-gwptd.conf,
конфиг примонтирован, пересборка не нужна:
| Адрес | Было | Стало |
|---|---|---|
mp.hyp.ru/ | 302 → /admin | 302 → /new |
mp.hyp.ru/admin | проксировался в gwptd-app | 302 → /new |
mp.hyp.ru/old | 302 → /admin | 302 → /new |
Как вернуть старый интерфейс — три способа, от быстрого к чистому.
Способ первый, мгновенный откат целиком (бэкап снят перед правкой):
ssh s2-int 'F=/data/hypertime/app/docker/nginx/conf.d/50-gwptd.conf
cp "$F.bak-pered-skrytiem-admin-20260809" "$F" \
&& docker exec new_hypertime_nginx nginx -t \
&& docker exec new_hypertime_nginx nginx -s reload'
Способ второй, вернуть только /admin, оставив корень на /new — если нужны оба
интерфейса параллельно. Убрать из файла блок:
location ^~ /admin {
return 302 https://mp.hyp.ru/new;
}
и перечитать конфиг (nginx -t && nginx -s reload). Тогда /admin снова проксируется в
gwptd-app:8000 общим правилом location /, а корень продолжает вести на /new.
Способ третий, доступ только для себя, без публичной дороги — туннелем, ничего не меняя в конфиге:
ssh -L 8100:gwptd-app:8000 s2-int
затем http://127.0.0.1:8100/admin.
⚠️ Проверять выдачей, а не конфигом:
for u in / /admin /old /new; do
printf "%-8s %s %s\n" "$u" \
"$(curl -s -o /dev/null -w '%{http_code}' https://mp.hyp.ru$u)" \
"$(curl -s -o /dev/null -w '%{redirect_url}' https://mp.hyp.ru$u)"
done
Почему это может понадобиться. Старый контур остаётся эталоном чисел: пока новый интерфейс не сверен по всем площадкам, сравнивать не с чем. И часть возможностей старого интерфейса в новый не перенесена — реестр того, что закрыто заглушкой, в §3.
2. Что теперь видит менеджер
Адрес один: https://mp.hyp.ru/new. Работают полностью три площадки:
| Площадка | Рейтинг | Оборачиваемость | Отгрузки |
|---|---|---|---|
| WB FBO (mp1) | ✅ | ✅ | ✅ |
| Ozon FBS (mp3) | ✅ | ✅ | ✅ |
| Lamoda (mp9) | ✅ | ✅ | ✅ |
Всё остальное закрыто заглушкой «в разработке». Заглушка закрывает три поверхности: страницу, обращение к Data API (отдаёт отказ, а не пустой успешный ответ) и задание в расписании. Пустой массив читался бы как «ноль продаж» — это хуже отказа.
На страницах расчётов видна дата данных и пометка, если отставание больше трёх суток.
3. Реестр заглушек — единственный источник правды
config/gwptd.php, ключ stubs. У каждой записи площадка, причина и основание (замер).
Добавить заглушку = дописать строку, снять = убрать.
| Площадка | Что закрыто | Почему |
|---|---|---|
| WB FBS (2), Ozon rFBS (4), YM FBY (8) | всё три | в фактах ядра пусто |
| Ozon FBO (5) | оборачиваемость | нет данных в fact_turnover |
| YM FBS (6) | отгрузки | нет данных для расчёта |
| Золотое яблоко (10) | всё три | см. ниже |
⚠️ Золотое яблоко — главная находка. Отгрузки там БЫЛИ: две партии от 07.08, 1713 строк, 75 штук на 1,25 млн ₽. И у всех 1713 строк рейтинг нулевой, у 1671 нулевые продажи. Отгрузка ранжирует по рейтингу, а ранжировать было нечем. Показать это менеджеру значило бы дать ему отгрузить по выдуманным числам.
Механизм: app/Services/StubService.php, app/Filament/Concerns/RedirectIfStubbed.php,
app/Filament/Pages/DevelopmentStubPage.php. Тесты — tests/Feature/Stubs/.
4. Разбор потери данных: 16 300 строк за сутки
Сбор получал строки и не раскладывал их. Ядро отказывалось публиковать факты
(rows_parsed_received_mismatch), поэтому оборачиваемость стояла с 3 августа. Отказ
ядра сработал как защита: неверных чисел никто не увидел.
Четыре причины, все — следствие доверия выборке:
- Список уходил параметром.
_bindразворачивал список в отдельные параметры вместо одного значения — четыре разных сообщения об ошибке от одной причины. - Тип выведен по сотне записей. У Ozon
dimensions[].idбывает датой рядом с числовым артикулом; у Золотого яблока опознаватели — UUID, а цена приходит объектом. - Дата площадки объявлена как
DATE, а приходит строкой ISO сZ. Поле площадки хранится КАК ПРИШЛО: преобразование — производное. parent_id/item_indexбыли обязательными. Это понятие ЛОКАЛЬНОЙ раскладки; сборщик связывает потомков деловым ключом и их не заполняет — терялось 108 из 108.
Записано миграцией marketplace-collector-v3/kernel/schemas/migrations/2026-08-09_intake_types_from_real_values.sql,
идемпотентность проверена двумя прогонами. ⚠️ ADD COLUMN IF NOT EXISTS — синтаксис
MariaDB, MySQL 8 отвечает 1064.
Итог: все затронутые эндпоинты разбираются чисто, ошибок разбора ноль.
5. Договор вывода: правка описания требует ОДНОГО свежего сбора
Ядро сверяет расписку прогона с текущим договором. Правка описания или схемы обесценивает все прежние расписки, и ядро честно отказывает, пока эндпоинт не пересобран.
Причины идут ступенями; смена причины означает, что предыдущая закрыта:
| Сообщение | Что означает |
|---|---|
spec_digest_mismatch | описание изменилось, данные по старому |
parser_errors_nonzero | разбор падает |
output_digest_mismatch | добавлены колонки |
output_contract_mismatch | набор таблиц в расписке ≠ объявленному |
Список затронутых даёт сам отказ: grep -oE "incomplete: .*" в журнале пересчёта.
Дефект, найденный 10.08 и починенный. Движок вложенного разворота писал в дочерние
таблицы, а расписка их не объявляла — add_output в spec_runtime/runner.py не спускался
в explode.children. Прогон записывал две таблицы при четырёх. Починено рекурсией плюс
kernel/etl/build_input_evidence.py, где ожидаемый список строился только из tables,
без intake_only.
От этого зависели и приведены в соответствие: манифест Золотого яблока
(specs/kernel/goldapple-kernel-inputs.yaml), функциональный снимок схемы
docs/collectors/SCHEMA-SNAPSHOT-2026-07-13.sql (в нём не было 63 дочерних таблиц вовсе),
рубеж матрицы стоков, жёсткие ожидания в тестах расписки.
6. Выкладка s3: роняет службу на последнем шаге
Четыре раза подряд одно и то же. Внутренняя выкладка завершается успешно — код
доезжает, контейнеры собираются и здоровы. Гибнет обёртка после: переиндексация
справки (activate-s3-mcp-gwptd.sh, 20 713 документов, 30–40 мин) не укладывается в срок.
Обработчик выхода пишет completion_unproven, изымает расписание, гасит контейнеры.
Восстановление (проверено):
ssh s3-int 'docker start gwptd-data-api gwptd-app-new
docker update --restart unless-stopped gwptd-data-api gwptd-app-new
crontab /opt/gwptd-analytics/repo/marketplace-collector-v3/scripts/crontab.s3-next.txt
T=/etc/gwptd/state/s3-next-deploy-transaction.env
[ -e "$T" ] && mv "$T" "$T.razobran-$(date +%H%M)"'
Расписки откладывать с пометкой, не удалять — они доказательство произошедшего.
⚠️ Перед повторной выкладкой обёртка требует точного состояния текущего рантайма:
образ = метка :next, здоровье healthy, политика перезапуска unless-stopped. После
ручного подъёма политика остаётся no, и выкладка отказывает с «runtime identity/health is
not exact». Лечится docker update --restart unless-stopped — и это не формальность: при
no контейнеры не поднимутся после перезагрузки хоста.
⚠️ SHA в --expected-sha брать целиком из git rev-parse HEAD. Совпадение первых
восьми знаков ничего не значит: обёртка сверяет все сорок и отказывает с «fetched branch
SHA differs».
Нерешённый вопрос владельца: убрать переиндексацию из пути выкладки. Она снята с автозапуска как ненужная, но каждую выкладку роняет службу. Это правка защищённой обёртки на сервере — без слова владельца не делать.
7. Состояние на утро 10.08.2026
| Код на s3 | 257fb492 |
| Контейнеры | оба живы, политика unless-stopped |
| Расписание | 80 строк, сторож автономности активен |
/new публично | отвечает |
| Заглушки | проверены в живом контейнере |
| Набор тестов сборщика | 17 падений (2 требуют живой базы) |
| Набор интерфейса | 176 зелёных |
| Оборачиваемость | 3 августа — ждёт пересчёта |
| Рейтинг | август |
| Не покрыто листьев | 23 из 1266 |
Идёт прямо сейчас: пересбор тринадцати эндпоинтов под новым договором, за ним пересчёт
ядра — цепочка /tmp/recollect-then-kernel.sh на s3, журнал /tmp/recollect.log. Это
последнее, что отделяет три площадки от свежих чисел.
8. Локальная база разбора — отдельная работа, готова
mp_hyp_ru на 127.0.0.1:33061 (контейнер mp-workshop-mysql): 175 таблиц, 3455
колонок, 1 054 194 строки, собрана переразбором накопленного сырья без опроса площадок.
Смотреть — Adminer http://127.0.0.1:8011, учётная запись mp_viewer (только чтение),
пароль в ~/.gwptd/mp_hyp_ru-viewer.txt.
Программы: marketplace-collector-v3/scripts/generate_schema_from_json.py (порождает схему
из анализа JSON), replay_raw_into_schema.py (раскладывает сырьё по порождённому перечню),
json_leaf_walk.py (общий обход листьев со значениями).
⚠️ Предел строки InnoDB — 8126 байт, а не объявленные MySQL 65535, плюс накладные на
каждую колонку. Широкие varchar переводятся в text.
10. ⚠️ ЕДИНСТВЕННОЕ, ЧТО ОСТАЛОСЬ: решение владельца по конфликту опознавателя
Ядро прошло все заставы входов — пять классов отказа закрыты подряд, и каждый сменялся
следующим, то есть закрывался. Теперь оно падает ВНУТРИ расчёта, на шаге
resolve_identifiers:
IdentifierProjectionError: candidate identifier conflicts with an existing mapping: 1
build_all stopped after failed step: resolve_identifiers
Что это значит. Ровно ОДИН опознаватель товара, вычисленный этим прогоном, спорит с уже сохранённой привязкой: та же площадка, тот же тип опознавателя, то же значение — но другая модель. Резолвер отказывается молча перезаписать привязку и останавливает сборку. Это правильно: durable-привязка артикул↔модель — то, на чём держится весь учёт.
Есть штатный путь, и он аудируемый. Флаг окружения
KERNEL_ALLOW_IDENTIFIER_REMAP (marketplace-collector-v3/kernel/etl/resolve_identifiers.py)
включает перенос привязок, и включает его ИЗБИРАТЕЛЬНО: переезжают только конфликты,
объяснённые pick-ом того же прогона (сохранённая модель ПРОИГРАЛА детерминированному
выбору по тому же идентификатору). Необъяснённые конфликты по-прежнему валят сборку —
механизм fails closed, а не «разрешить всё».
Известный предшественник (21.07.2026): marketSku Яндекса на два shopSku при
перевыставлении карточки — для Яндекс.Маркета это структурно легально. Тогда и появился
детерминированный picker: 1) uuid_1c IS NOT NULL (настоящая модель 1С) важнее
lazy-promoted, 2) MIN(model_id) — базовый артикул важнее варианта «_N».
Решение владельца, которое нужно назвать явно: включать ли
KERNEL_ALLOW_IDENTIFIER_REMAP для этого прогона.
- За: это единственное, что отделяет три площадки от свежих чисел; путь штатный, аудируемый и не пропускает необъяснённые конфликты; предшественник признан легальным.
- Против: перенос привязки меняет, к какой модели относится артикул площадки. Если конфликт НЕ из-за перевыставления карточки, а из-за ошибки сопоставления, перенос закрепит ошибку в durable-таблице.
Что я делать не буду без слова владельца: включать флаг. Это не технический выбор, а решение о том, чей артикул чьей моделью считать.
Что можно сделать без решения: посмотреть сам конфликт. Временная таблица кандидата
удаляется вместе с прогоном, поэтому нужен прогон с сохранением кандидата — либо разбор
dim_product_identifier против свежего приёма по типам marketSku/shopSku (mp6),
nmID/vendorCode (mp1), offer_id/product_id/sku (mp3), barcode (mp0).
9. Что делать дальше по порядку
- Дождаться цепочки пересбора и убедиться, что оборачиваемость сдвинулась с 3 августа.
- Если ядро отказывает снова — взять список из
incomplete:и пересобрать названные эндпоинты. Это конечная процедура, а не тупик. - Закрыть два дефекта описаний, вскрывшихся из-под прежних ошибок:
ozon.postings_fbo— «different field sets by kind»;goldapple.snapshot— расхождение двух компиляторов (load_runtime_product≠load_product). - Решение владельца: убрать переиндексацию из пути выкладки (§6).
- Остаток 23 листьев — только по запросу бренд-менеджера. Принцип владельца: нужность определяет тот, кто работает с данными, а не мы.