media/save, который переписывает ВЕСЬ набор медиа карточки — видео в массив не входило, поэтому удалялось. Это не ограничение WB, а неправильный выбор методаmedia/file (загрузка одного фото на конкретную позицию через заголовок X-Photo-Number). Он меняет только указанную позицию и не трогает остальные медиа. Нумерация прямая: №1 = главное фото (обложка), №2 = второе и т.д. Видео хранится у WB отдельно и при замене фото не затрагиваетсяmedia/file X-Photo-Number=1 → главное изменилось, а видео-автообложка (is_autoplaying_video, автозапуск) полностью сохранилась. То есть сохраняется не просто наличие видео, а именно режим обложкиmedia/file: убрана прежняя заглушка «пропускать видео-карточки», добавлена функция точечной смены главного фото. Видео-карточки снова участвуют в ротации. Полный боевой проход по всему парку: 118 карточек ротировано, 0 ошибок. В админке зелёная отметка «у этой карточки есть видео — смена фото работает, видео сохраняется»media/file подкинул сторонний ChatGPT (через менеджера); диагностику нумерации, проверку на автообложке и внедрение в сервис сделали сами. Урок зафиксирован: не упираться в один метод API и не объявлять «невозможно» раньше времениhas_video=true каждый час, отказов модерации нет. Но вскрылась настоящая проблема: видео теряет статус автообложки (is_autoplaying_video) — превращается из первого автозапускающегося слайда в обычный слайд галереи. Это и есть жалоба менеджера по 845297131: «видео слиплось с главной, в приложении вместо главной слайд воронки»media/save (а это единственный способ сменить главное фото) превращает видео-автообложку в рядовой слайд. Проверил экспериментом — даже видео первым в массиве флаг автообложки не возвращает. Нетронутый эталон (845291793) держит is_autoplaying_video=true, все карточки, которых касался media/save, — теряют его. Автообложку WB ставит ТОЛЬКО при ручной загрузке видео через кабинет, через интеграцию — никакbasket-NN.wbbasket.ru/.../info/ru/card.json → поле media.has_video). За 9 часов БЕЗ нашего вмешательства видео сохранилось. Затем прямым тестом на одной карточке: сменил фото как ротация → через 2 минуты видео слетело. Вывод однозначен: видео стирает именно наша смена фотоPOST /content/v3/media/save, которым ротатор меняет главное фото, заменяет ВСЕ медиа карточки тем, что мы передали. Мы передавали только фото → видео (которое не передаём) стиралось. А cards/list ссылку на видео не отдаёт — взять её неоткуда.m3u8 WB игнорит, файл-оригинал 9-10 МБ отвергает (409), а сжатый .mp4 до ~5 МБ (1080p) WB принимает и видео остаётся. То есть если в каждую смену фото вкладывать сжатое видео — фото меняется, видео живёт. Проверено от и до на реальной карточкеctr_rotate.py): добавил кеш has_video в БД (проверка через CDN раз в сутки) и колонку original_video (URL сжатого видео). Логика: есть видео + есть его сжатый URL → крутим, вкладывая видео в каждый запрос; есть видео без URL → пропускаем (не сносим); видео нет → крутим как обычноscripts/ctr_service/ctr_rotate.py: при ротации тянет с WB через POST /content/v2/get/cards/list поле photos, и отправляет обратно через POST /content/v3/media/save body {nmId, data: [url1, url2, ...]}. Поле data — единый массив, включающий и фото и видео по схеме WB. Мы туда видео не клали, потому что не знали про негоcards/list на живой карточке Весны (nm_id=706828973). В ответе ключи: brand, characteristics, dimensions, imtID, nmID, nmUUID, photos, sizes, subjectName, title, vendorCode и т.п. — ни одного поля про видео. Метод cards/list в принципе не возвращает URL видео. Получается схема несимметричная: media/save требует передавать все медиа (включая видео), а cards/list видео не отдаёт. Получить URL видео для отправки обратно — нечемdata должен содержать ВСЕ существующие медиафайлы карточки, иначе пропущенные стираются. Видео мы не передавали → видео стиралось при каждой ротации*/30 * * * * ctr_rotate.py — WB-ротация остановлена полностью. Сбор статистики (ctr_collect_vps) оставил, он только читает. Бэкап crontab уже есть от 09.06POST /api/tests/{id}/start один раз через WB API получить и сохранить URL видео карточки в ctr_tests.original_video (новая колонка); (2) в do_rotation после массива фото добавлять URL видео из БД в data перед отправкой; (3) поискать в WB Swagger отдельный метод для чтения видео — если есть, отказаться от ручного шага; (4) бейдж в админке «у карточки есть видео» чтобы менеджер видел где перепроверить/var/log/ctr_ozon_rotate.log массовые ошибки HTTP 400 {"code":3,"message":"VALIDATION ERROR"} на эндпоинт /v1/product/pictures/import. Из последнего прогона: «ротировано 14, ошибок 12». В БД ротация записывалась даже при ошибке Ozon — отсюда расхождение «админка показывает что сменилось, а на сайте старое»/v1/product/import-by-sku вернул 200 с task_id, но статус задачи через /v1/product/import/info = skipped (этот эндпоинт для атрибутов, не для фото)/v3/product/info/list: на одной из тестовых карточек (pid 873932301) Ozon вернул statuses: { status: 'variant_wait', moderate_status: 'declined', status_name: 'Отклонён' } и errors: [code:'image_not_upload', code:'DESCRIPTION_DECLINE']. Карточка отклонена модерацией — Ozon перестаёт принимать любые правки фото*/30 * * * * ... ctr_ozon_rotate.py в crontab — ротация Ozon остановлена полностью. Сбор статистики (collect) оставил. Бэкап старого crontab в /root/crontab_backup_2026-06-09.txtmoderate_status карточки через info/list — если declined или в модерации, ставить тест на паузу автоматически; (2) частоту ротации перевести с 1.5 ч на 1 раз в сутки минимум; (3) в админке добавить визуальный бейдж «отклонено модерацией» рядом с тестом, чтобы менеджер сразу видел затыкvideos в запросе (если такое есть в новой схеме), либо менять эндпоинтmoderate_status=declined из скобок, заменить product_id на supplier_article потому что в Seller-кабинете ищут по артикулам а не по internal id, добавить когда сервис включится обратно). После правок ✓LATERAL VALUES + сборка по месяцам через FILTER), все исходные расчёты сохранены 1:1product_chars), цена/скидка/СПП (orders), себестоимость (stock_cost_4stores), комиссия категории и тарифы возврата (tariffs_*), коэффициенты склада (acceptance_coefficients). Громоздкая формула логистики, которая в Excel тянет индексы из разных мест, у нас сворачивается в один JOINitem_type='question' внутри review_monitor.py + review_bot.py), отдельного бота не былоquestion-bot на VPS. Логика и база данных остаются общими с ботом отзывов (не дублируем) — разделён только канал доставки и обработчик нажатий. В review_monitor.py добавлена маршрутизация route_bot(): вопрос + Бумбокс → новый бот, всё остальное (отзывы + вопросы ИП/Весна/др.) → @packmen_bot как раньшеquestion_bot.py — на базе бота отзывов, но со своим токеном, своим приветствием и статистикой /stats только по вопросам (фильтр по новой колонке review_queue.target_bot). Сразу с потоковой обработкой (ThreadPoolExecutor), которую недавно внедрили в бот отзывов — нажатия не блокируют друг друга. Публикация ответов на вопросы — через тот же write-токен Бумбокса (scope=128)target_bot через BEGIN/ROLLBACK, чистый старт сервиса (подключился как @voprosklient_bot, long-polling). Бэкап монитора до правок. Доставка копии мне проверена живым сообщением. Обязательный шаг для запуска — Дарья и я нажимаем Start у бота (Telegram запрещает ботам писать первым); я свою часть активировалwb_questions (entity='бумбокс') извлечены артикулы (формулировок много: «персональный артикул», «Создали специально для вас… Артикул N», «Макет изменён. Артикул:» — поэтому брал все упоминания, а не одно ключевое слово). Артикул сверял с каталогом карточек WB cards_list (12 497 карточек Бумбокса) и брал дату создания карточки createdAt — это момент, когда наклейку реально сделали. Далее — заказы (wb_daily_orders, только неотменённые) и выкупы (wb_daily_sales) по этому nm_idcollected_date min = 12.02, январь подгрузился частично — 9–278 строк/нед против 15 000+/нед с февраля). То есть январская «низкая конверсия» — артефакт неполного сбора, а не реальность. Окно анализа честно ограничено 09.02–05.06.2026. Помесячная стабильность 72–78% подтвердила корректностьSELECT original_primary, original_gallery FROM ctr_ozon_tests WHERE id=2067 — orig_primary не пересекается по URL с галереей, значит проблема в логике сборки, не в данных; (2) SELECT * FROM ctr_ozon_variants WHERE test_id=2067 — у активного теста ИП Галимзяновой 6 вариантов: 4 тестовых + ДВА «Основное фото» (id 833 и 834, аномалия)ctr_ozon_rotate.py:299: при формировании массива фото для Ozon код сравнивает orig_primary != new_main ПО URL-СТРОКЕ. У is_original-варианта new_main = /ctr-ozon/uploads/ctrz_X_orig_Y.jpg (наша скачанная копия), а orig_primary = https://ir.ozone.ru/.../e82fa....jpg (URL с Ozon CDN). Это визуально одно и то же фото, но URL разные → дедупликация не срабатывает → Ozon принимает оба на позиции 1 и 2 и показывает как дубль. На не-is_original вариантах проблемы нетand not nxt.get('is_original') в проверку перед full.append(orig_primary). Если ротация идёт на «Основное фото», исходный primary в галерею больше не добавляется — наш файл уже является его визуальной копиейctr_ozon_rotate с моком set_pictures (печатает массив без реальной отправки на Ozon), принудительно вызвал ротацию на is_original вариант 833 теста 2067. Результат: 17 фото в массиве, ноль дублей по URL. До фикса было бы 18 фото с orig_primary на позиции 2ctr_ozon_rotate.py залит на VPS в /root/scripts/ctr_ozon_service/. Скрипт запускается напрямую по cron (не в Docker), поэтому контейнер не нужно перезапускать — следующий запуск ротатора уже подхватит исправленный код. Дубль на текущей карточке исчезнет при следующей ротации (~через 90 мин), отдельно предупредил менеджера про Ozon CDN-кэш 15-30 мин на применениеPOST /api/tests/{id}/restart в scripts/ctr_service/ctr_api.py. Удалил цикл копирования старых вариантов и копирование файлов через shutil.copy. Вместо этого вызываю _save_original_to_local(new_test_id, photo) — тот же helper что использует start_test при автодобавлении оригинала. В новый тест попадает СВЕЖЕЕ основное фото с WB (на случай если за время прошлого теста менеджер успела сменить главное фото карточки), а не копия старого файлаctr_tests есть частичный UNIQUE (org_id, nm_id) WHERE status != 'completed' — больше одного незавершённого теста на (org, nm_id) быть не может. Раньше если на карточке уже висел черновик (от прошлого перезапуска), restart падал с 500 «Не удалось создать тест». Скопировал логику из Ozon CTR (которую делал 28.05): если незавершённый дубликат есть — переиспользуем его (DELETE варианты + UPDATE test_name/status='draft' + сброс target_views/started_at/completed_at/final_snapshot/linked_campaign_id), иначе INSERT нового. Теперь restart срабатывает в любом случаеctr-api обновлён через docker cp + docker restart, образ зафиксирован через docker commit ctr-api:latest (если контейнер перезагрузится — поднимется с новым кодом)POST /api/tests/27860/restart — ответ {ok:true, new_test_id:27862}. SELECT из ctr_variants WHERE test_id=27862 вернул ровно 1 строку: label='Основное фото', is_original=t, status=active. То есть 9 старых вариантов в новый тест НЕ перенеслись, остался только свежий ОригиналorderStatus со значениями buyout (выкуп), rejected (отказ на пункте выдачи), returned (возврат). Замер по ИП/Весна/Бумбокс: из ~3000 отзывов 2905 выкуп, 53 отказа, 43 возврата. Поле теперь тянется в БД (новая колонка review_queue.order_status), в промпт AI и в карточку Telegramtest_review_prompt_ab.py, который берёт живые отзывы WB с разными статусами и генерирует ответ старым и новым промптом для сравнения. На отказе 5★ старый промпт писал «модель будет радовать Вас долгие годы, желаем приятного использования», новый — «спасибо за оценку и интерес, будем рады видеть Вас снова». Перед деплоем — бэкап прод-файлов, после — регрессионная проверка (валидность INSERT через BEGIN/ROLLBACK, прогон монитора без ошибок, чистый перезапуск бота)getUpdates. Пока шла публикация ответа в WB (таймаут до 30 сек) или перегенерация через AI, цикл был заблокирован: новые отредактированные сообщения ждали очереди (отсюда задержка), а нажатие «Опубликовать» в этот момент не успевало обработаться и протухало у Telegram (кнопка «грузит-грузит и слетает»). Перевёл главный цикл на многопоточную обработку (ThreadPoolExecutor, 6 потоков): теперь каждое нажатие и сообщение обрабатывается параллельно, основной цикл мгновенно возвращается к опросу Telegram и остаётся отзывчивым даже во время публикации. Обработка сообщений вынесена в отдельную функцию, режим ожидания текста при редактировании сделан потокобезопасным. Деплой с бэкапом, бот перезапущен и стабилен (0 падений)ctr_ozon_tests поменял дефолт колонки rotation_interval_min с 30 на 90, и одной транзакцией обновил все 100 активных и незавершённых тестов (UPDATE ... WHERE status NOT IN ('completed','archived')). Завершённые 93 теста не трогал — это история. Cron-расписание ротатора оставил прежним (каждые 30 мин), потому что функция needs_rotation() сама проверяет «прошло ли N минут с прошлой ротации» — менять можно только колонкуctr_ozon_api.py) добавлены два эндпоинта: POST /api/tests/bulk-pause и POST /api/tests/bulk-resume, оба принимают опциональный org_id. Bulk-resume берёт только тесты со started_at IS NOT NULL — черновики, которые ни разу не запускались, не трогает (иначе менеджер случайно «запустила бы» сразу 100 нетестированных карточек). В UI в правом верхнем углу появились две кнопки «⏸ Пауза всех» и «▶ Запустить все паузы». Если в фильтре выбрано конкретное юрлицо, в подтверждающем окне видно что действие сработает только по немуPOST /api/tests/{id}/archive и POST /api/tests/{id}/unarchive. В UI: пилюля «Архив» в верхнем фильтре статусов, иконка-папка «🗄» в каждой строке (с event.stopPropagation() чтобы не открывался детальный экран), кнопка «🗄 В архив» / «⤴ Вернуть из архива» в шапке детальной страницы. По умолчанию архивные не попадают в дефолтный список («Все») — это отдельный фильтрdocker build делать не стал — обновил код в работающем контейнере через docker cp ctr_ozon_api.py + docker restart, затем зафиксировал состояние через docker commit ctr-ozon-api ctr-ozon-api:latest (если контейнер перезагрузится, поднимется уже с новым кодом). Проверку UI прогнал через playwright по правилу из памяти (форматирование числе и фильтры через API дают false positive): сделал 3 скриншота — главная (пилюля «Архив», обе массовые кнопки, колонка 🗄), детальная страница (кнопка «🗄 В архив»), архивный фильтр (счётчики Все 192 / Архив 1, бейдж «В архиве» серый, иконка ⤴)archive вернул {previous_status:'draft'}, счётчик counts отразил Архив=1 и Черновики=99 (было 100), запрос с ?status=archived показал именно эту карточку, unarchive вернул её в Черновики. Bulk-pause с несуществующим org_id=999 вернул affected=0 — лишних UPDATE нетarray_to_string(gallery, '||') для склейки URL-ов одной строкой, и затем в Python код делал split('|'). Но утилита psql -A (режим без выравнивания) экранирует символ | внутри значений как \|, из-за чего разделение строки на части ломалось ровно на первом URL. Заменил array_to_string на to_json(...)::text и разделитель полей на табуляцию — теперь читается ровно 25 URL, проверено на тестовой карточке #2069original_gallery в БД через psql, (3) выгрузил тот же SELECT через тестовый скрипт test_parse.py и увидел, что Python видит только 1 URL вместо 25 — это и есть точная точка сбояctr_ozon_tests.original_gallery, original_primary) — могу одной командой вернуть полный исходный набор фото на Ozon по всему списку, либо отдать менеджеру Excel со ссылками «было / стало» для ручной сверкиscripts/ctr_ozon_service/ctr_ozon_rotate.py и через docker commit перевыпустил образ ctr-ozon-api:latest (Docker Hub в этот момент возвращал 429 rate-limit, поэтому собрал образ из работающего контейнера), (3) проверил парсер на живой карточке — 25 URL, корректный primary, (4) включил cron обратно, (5) первая ротация после фикса прошла штатно — на Ozon ушёл полный набор176.57.67.104 (IP Tilda) на 195.133.77.70 (наш VPS) по подготовленной инструкцииcloud-api.yandex.net/v1/disk/public/resources/download. Файл .mov (Apple QuickTime — плохо играется в Android-браузерах) сконвертирован в .mp4 H.264/AAC через ffmpeg с флагом +faststart для быстрого старта в браузере (44 МБ → 18 МБ)/home/webuser/project/website/kzshelp/ — синий градиент шапки, логотип «Казанские смесители» (КС) слева в белом бэйдже с тенью, рядом заголовок «Видеоинструкции». 5 карточек видео с эмодзи-иконками, по клику открывается модальный плеер HTML5 <video controls playsinline>. Корректно отображается на десктопе (макс 720px) и мобильном (375px) — критично для пользователей со сканированными QR-кодами+7 912 028 37 79 (кликабельный tel:), email KZS-market@yandex.ru (mailto:), три кнопки маркетплейсов с переходом в новой вкладке: Wildberries (/seller/4010491), Ozon (/seller/kazanskiy-zavod-smesiteley), Яндекс Маркет (/business--kazanskii-zavod-smesitelei/109294089). Логотип вырезан из присланного скриншота через PIL: верхняя треть → белый фон в прозрачный → bbox-обрезка непрозрачных пикселейserver_name kzshelp.ru www.kzshelp.ru в существующий контейнер nginx:alpine. HTTP-блок отдаёт ACME-challenge и делает 301-редирект на HTTPS. HTTPS-блок раздаёт статику из /usr/share/nginx/html/kzshelp/ (смонтирована из website/kzshelp/). Перед каждым изменением — бэкап nginx.conf с timestamp, после — nginx -t и nginx -s reloadkzshelp.ru и www.kzshelp.ru через certbot --webroot -w /home/webuser/project/website (тот же webroot, что и для остальных 7 доменов). Сертификат валиден до 13.08.2026, автопродление через системный certbot.timer уже работает. Зелёный замок в браузереnslookup kzshelp.ru 8.8.8.8 уже показывал новый IP. Причина: системный DNS-резолвер Windows кешировал старую запись (видна через ping kzshelp.ru → старый IP). Решение: ipconfig /flushdns сбросил кэш, после чего сайт открылся корректноscripts/: setup_kzshelp_http.py (nginx + заглушка), setup_kzshelp_content.py (скачать + конвертировать видео + собрать HTML), setup_kzshelp_ssl.py и setup_kzshelp_ssl_v2.py (выпуск SSL + переключение на HTTPS), update_kzshelp_v2.py (логотип + порядок видео + футер), update_kzshelp_header.py + _center.py (горизонтальная шапка с центрированием), extract_logo.py (PIL-обрезка логотипа из скриншота)wb_questions (entity='бумбокс'), за период 15.11.2025 — 14.05.2026 взято 6 815 обращений по 2 550 уникальным SKU. Создан скрипт scripts/analyze_boombox_questions.py (TF-IDF + словарные правила, 20 категорий). Результаты выгружены в analysis/boombox_questions_faq.xlsx (22 листа: сводка, триггер-слова, по листу на категорию) и boombox_questions_faq.mdis_order_brief(): считаем слоты (размер \d+х\d+, шрифт шрифт N, цвет из словаря) — если ≥2 параметра ИЛИ есть префикс заявки + 1 параметр, классифицируем как заявку. На такие обращения бот не должен отвечать FAQ-шаблонами — нужна эскалация на дизайнера≥3 слотов → только дата+поверхность, 1-2 → дозапрос недостающих, 0 → полная воронкаanalysis/boombox_bot_funnel_proposal.xlsx, 9 листов: «О предложении» (цель, инсайты, что нужно от менеджеров), «Архитектура процесса» (7 этапов от WB до производства), «Цифры из анализа», «Воронка вопросов» (формулировки бота на каждом шаге), «Кнопки и варианты» (что менеджеры могут править), «Сценарии» (10 типовых ситуаций), «Карточка ответственному», «База знаний AI» (12 FAQ-категорий с готовыми ответами), «Чек-лист согласования» (12 пунктов на утверждение)analysis/calls/dasha_call_2026-05-15.{webm,wav,vtt,txt}scripts/build_bot_funnel_proposal.py, формирует Excel из 9 листов с форматированием (заливка приоритетов, переносы строк, замораживание шапок). Скрипт идемпотентный: переписывает файл целиком, можно перегенерить после правок логикиget_customer_history(), которая для каждого нового вопроса/отзыва подтягивает до 5 предыдущих сообщений того же клиента (по customer_name + nm_id) за последние 14 дней. Используется и в промпте AI, и в Telegram-карточке. Решает кейс «дробных» заказов: клиент пишет в 3 сообщениях текст → шрифт → цвет — AI собирает всё в один связный ответentity='бумбокс' и item_type='question' в системный промпт DeepSeek подмешивается специальный блок BUMBOKS_QUESTION_CONTEXT: 4 обязательных параметра индивидуального заказа (текст / размер в см / цвет / номер шрифта 1-31), материал Oracal 641, сроки 3-5 дней, правила что делать если параметров не хватает или если клиент просит аналог карточкиresponse_templates (id 11-16, category='bumboks'): «нужны параметры заказа», «заказ принят», «нужен шрифт и цвет», «про аналог по карточке», «про материал и нанесение», «сроки доставки». Дарья может одной кнопкой отправить готовую заготовку/root/scripts/review_monitor.py.bak_20260507_2202. Скрипт прошёл синтаксическую проверку через ast.parse локально и на VPStranscripts/5467640573517141177.txt#contact) переделаны: каждая карточка теперь содержит QR-код сверху + иконку, название и контакт. Клик по карточке открывает чат, скан QR — то же самое с любого телефона. Сгенерированы 3 QR через Python qrcode: WhatsApp, Telegram, MAX. Файлы: /images/qr/{telegram,whatsapp,max}.png@dvorec_masterov → новый @dvorec_masterov_stickers (по QR-коду от Фариды). Обновлено и в карточке, и в футере сайтаwa.me/7XXXXXXXXXX на реальную wa.me/79992667114grid-template-columns: repeat(3, 1fr). Переопределено на repeat(2, ...) с max-width: 720px; margin: auto — теперь карточки строго по центру301 → HTTPS без исключения для /.well-known/acme-challenge/, плюс у metabase и studio в renewal-конфиге был authenticator = standalone (не работает пока nginx занимает 80 порт)location /.well-known/acme-challenge/ в server-блок 80 порта для редиректа, у metabase/studio в /etc/letsencrypt/renewal/*.conf поменян authenticator на webroot. Теперь certbot.timer (twice daily) будет успешно продлевать все сертификаты автоматическиopenCardCount, заходы из выдачи в карточку), «CTR корзины» → «CR в корзину» (это Conversion Rate, не CTR), «CTR (рекламы)» — настоящий CTR, считается только для тестов с активной РК (берём показы и клики из рекламной статистики WB)/root/scripts/ctr_service/ctr_api.py и admin.html на VPS — оба обновлены 04.05.2026 11:44 UTC. Ротатор и сборщик данных не трогали — они работают по cron как обычно (*/30 мин ротация, */6 ч сбор)ctr_organizations.wb_content_token отдельно от токенов сбора данных. Ротатор использует POST /content/v3/media/save: получает текущие 25 фото карточки → меняет позицию #1 на нужный вариант → отправляет полный массив URL обратно. На тесте #26 (СДВ КС 101254) Вариант A впервые реально появился на карточке Wildberries в выдаче и на странице товараctr_rotations), и дневные просмотры/корзина/заказы делятся между вариантами пропорционально времени. Лидер по CTR корзины подсвечивается зелёным с пометкой «Лучший». Исправлен баг сравнения округлённых значений — раньше ни один вариант не помечался лидером из-за разницы между точным и округлённым CTR/api/variants/upload-batch отправляет все файлы за раз, имена назначаются автоматически (Вариант 2, Вариант 3, ...)target_views (например 10 000 для базы, 50 000 для надёжного результата). Ротатор перед каждой ротацией проверяет: если суммарные показы карточки достигли цели — тест автоматически завершается. Можно оставить пустым — будет работать бессрочноctr_variants.status='paused'). Если активных вариантов меньше 2 — ротация по тесту приостанавливается. На уровне теста сохранена кнопка «Пауза» — останавливает всю ротациюctr-api в Docker-сети сменился (172.18.0.11 → 172.18.0.7), а в nginx был захардкожен старый IP. Переписал на resolver 127.0.0.11 + имя контейнера, теперь устойчиво к перезапускамctr_tests (test_name, target_views, started_at, completed_at), ctr_organizations (wb_content_token), ctr_variants (status). Заполнены started_at для ранее активных тестов. Всего 5 таблиц ctr_*: organizations, tests, variants, rotations, stats_rawozon_* в PostgreSQL, загружено ~11 500 строк исторических данных с августа 2025. Размер БД: 1370 MB → 1533 MBozon_realization (87 строк финотчёта), ozon_transactions (812 транзакций), ozon_cashflow + ozon_cashflow_details, ozon_cost_prices (978 строк себестоимости), ozon_paid_storage (1998), ozon_analytics_data (1967), ozon_clusters_fbo (2454), реклама (campaigns/expense/daily), возвраты, отправления FBO/FBSentity различает юрлица. Весна имеет 31 колонку в realization с детализацией лояльности (зелёные цены, АПВЗ), ИП — 27 колонок. Универсальный подход — взяли union заголовков, недостающие поля остаются NULL. Все колонки — TEXT для надёжной загрузки, типизация через CAST в view'хах Metabasefifo_auto_update.py (экономия AI-токенов), generate_ads_report_vps.py, generate_stock_report_vps.py, generate_deductions_report_vps.py, generate_pnl_report_vps.py, generate_reviews_report_vps.py, generate_reviews_stats_vps.py. Бэкап crontab: /root/crontab_backup_before_pause.txt. Автосбор данных и бот отзывов продолжают работуctr-api в Docker-сети поменялся (было 172.18.0.11, стало 172.18.0.7), а в nginx был захардкожен старый IP. Переписал nginx-конфиг на resolver 127.0.0.11 + имя контейнера — теперь устойчиво к перезапускам/data/ctr-uploads/ на хосте (она монтируется только в ctr-api). Добавлена location /ctr-test/uploads/ с проксированием на FastAPI, который уже отдаёт файлы. Загруженные варианты фото теперь корректно отображаются в админке/ctr-test/ (read-only дашборд) и /ctr-test/admin/ (управление). Теперь всё на dvorecmasterov.ru/ctr-test/: метрики + загрузка вариантов + запуск/пауза/завершение тестов. Cron генерации статического дашборда удалёнctr_rotate.py каждые 30 мин логирует смену варианта в БД (94 ротации за 3 дня по тесту #26), но запрос POST /content/v3/media/save на WB возвращает 401 Unauthorized. Последствие: на WB висит одно и то же фото, CTR по вариантам неразличимlimit=100, поэтому из 180 карточек отображались только 100 (ИП). После фикса видно все: ИП 100 + Весна 80 = 186 карточек в системеyadisk_scan_public.py + yadisk_list_all_pdf_v2.py: рекурсивный обход публичной папки «Бумбокс» через API Я.Диска с пагинацией по разным media_type (document, unknown, null). Итого 12 358 PDF-файлов собрано в JSON-карту с публичными ссылками формата disk.yandex.ru/d/<public_key>/<путь>match_articuls.py: 9 095 артикулов из Google Sheets (таблица 18KuBnNA0IEX, вкладка «готово в офисе», колонка D) сравнивались с именами PDF-файлов. Сгенерирован CSV с колонкой F для заливки обратно в таблицуwrite_to_sheets.py через Service Account sheets-bot@sticker-sheets: в колонку F таблицы «готово в офисе» добавлены кликабельные ссылки на PDF-этикетки для каждого артикула. Теперь у менеджера мгновенный доступ к этикетке прямо из таблицыisAnswered=true, но answer=None), не дожидаясь ответа продавца. Официальное подтверждение от поддержки WB: «Площадка считает такие оценки положительными и принятыми, а алгоритмы могут автоматически помечать их как обработанные для ускорения работы продавца.»fetch_archived_empty в review_monitor.py — раз в 30 мин после обычного прохода дополнительно обходит архив isAnswered=true, фильтрует: пустой текст + rating 4-5 + нет ответа + возраст 30мин–48ч. Для найденных — генерирует AI-ответ через DeepSeek и публикует через PATCH /api/v1/feedbacks/answer (поле answer.editable=true позволяет это)sheets-bot@sticker-sheets.iam.gserviceaccount.com (создан 14.04 для прошлой задачи)C:/Users/Анатолий/.openclaw/workspace/skills/ai-humanizer/ (24 паттерна признаков AI-текста, 500+ терминов AI-лексики, стат-анализ burstiness/TTR). Принципы применены при генерации отзывовdisk.read, disk.info), получен токен аккаунта KZS-market (544 GB / 1.1 TB). Создан Google Cloud проект sticker-sheets с Sheets API и сервис-аккаунтом sheets-bot@sticker-sheets.iam.gserviceaccount.com. Оба credentials сохранены в memory для повторного использования/v1/disk/resources/files с пагинацией. За ~3 минуты получен список всех PDF на диске. Сопоставление с артикулами из столбца D таблицы дало 8 631 уникальных совпадений (по 1 ссылке на артикул, дубликаты из разных заказов игнорируются)AUDIT_dvorec_analytics_2026-04-08.md, tables_classification_2026-04-08.mdmonthly_fbo_stocks.py берёт снимок из БД на 07:00 МСК 1-го числа, но за ночь уже прошли заказы нового месяца. Решение: переписать скрипт на прямые вызовы WB+Ozon API параллельно, cron перенести на 58 20 28-31 * * с проверкой завтрашнего дня (23:58 МСК последнего дня месяца)5382209941673123614.ogg (6:40) и 5382209941673123628.ogg (3:48) через faster-whisper модель medium на CPU. Зафиксированы ключевые решения: префиксная система в dvorec_analytics (! — рабочая, опи — API, R- — ручной ввод), себестоимость тянуть готовой из «Мой Склад» (не считаем сами), новые таблицы R-pnl_monthly и R-expenses для P&LТермо_Осман_корона_черн) — мастер-поле для сопоставления, F «ссылка шк» — целевой столбец, полностью пустой. Таблица открыта на редактирование по ссылкеhttps://disk.yandex.ru/d/... (превью PDF в браузере, работает у любого без логина)<артикул>.pdf ровно как в столбце D (например, Термо_Осман_корона_черн.pdf). Один артикул — один PDF. Источник — папка «Бумбокс / 1 (1). Заказы» с десятками тысяч подпапок-заказов, в каждой по ~3 PDF штрихкодов товаров из заказаdvorec_analytics вместо создания новой БД. Удалено 58 неиспользуемых таблиц: было 121, осталось 63. Размер БД: 1413 MB → 1370 MB. Удалены: 7 «сирот» wb_*, 11 bp_* (Блюпинк), 10 gsheet_* старых, 14 ip_* старых, 3 FIFO v1, 3 прочих, 10 пустых таблицwb_cards используется CTR-сервисом (ctr_collect.py), review_templates — ботом отзывов (review_monitor.py), fifo_v2_sku_mapping — FIFO-расчётом. Все три исключены из удаленияbumboks, ip_vesna, svezho с 87 view каждая удалены через DROP SCHEMA CASCADE. SQL-пользователи metabase_bumboks, metabase_ip_vesna, metabase_svezho удалены. Скрипт setup_data_isolation.py найден и сохранён — при необходимости разделение по компаниям пересоздаётся за минутуmetabase_reset.py (DRY RUN + --execute режимы). Удалены: 22 коллекции, 74 вопроса, 6 дашбордов, 4 лишних подключения к БД (ИП+Весна, Бумбокс, Свежо, демо H2). Сохранены: пользователи, группы, подключение «Аналитика Дворец». Бэкап БД metabase: /root/backups/metabase_before_reset_2026-04-09.dump (3.3 MB)dvorec_readonly (только SELECT, statement_timeout 60 сек). Попытка DELETE → permission denied. В Metabase добавлено: подключение «Дворец (read-only)», коллекция «Песочница стажёров», группа «Стажёры» с ограниченными правами. Стажёры могут создавать вопросы/дашборды в своей коллекции, но не могут модифицировать данные или видеть production-контентmetabase_hide_tables.py в обоих подключениях (admin и read-only) скрыты: AI-ассистент (12 таблиц), VPN-админка (4), CTR-сервис (5), старые wb_api_* (3), бот отзывов служебные (6), мелочи (2). Видимыми остались 31 таблица маркетплейс-аналитики + FIFO v2deploy_db_report_v2.py): динамическое получение размеров из БД, 10 групп таблиц с описаниями, 5 типов связей (entity, nm_id, barcode, sa_name, campaign_id + FK), разделы доступа и cron-автоматизации. Блоки AI-ассистента и VPN убраны из публичного отчёта/root/backups/before_cleanup_2026-04-08.dump (132 MB), скачан локально. Все удаления выполнялись в транзакции BEGIN/COMMIT. Проверены все cron-скрипты: collect_daily, collect_ad_stats, collect_wb_extended, review_monitor, load_wb_realization, fifo_auto_update, ctr_collect, ctr_rotate — всё работает, ни одной ошибки «relation does not exist»dvorec_main с нуля оказалось слишком сложно. Решено: оставаться в dvorec_analytics, сначала почистить от мусора, потом строить новые дашборды. dvorec_main остаётся как backup, не развивается. Обнулить Metabase, строить дашборды с нуля — только самые необходимые5382209941673123614.txt, 5382209941673123628.txt). Ключевые пожелания: характеристики товара (ежедневная синхронизация), план развития/анализ продаж (артикул × недели × заказы/продажи/реклама/негативные отзывы), P&L по месяцам, расходы (ручной ввод), себестоимость из «Мой Склад» (тянем готовое, сами не считаем)sales_ozon (dvorec_main)Бумбокс и ИП Галимзянова — разные юрлицаctr-api (FastAPI, порт 8100), подключён к Nginx через project_default сеть. 5 таблиц PostgreSQL с префиксом ctr_. Cron: сбор */6ч, ротация */30мин, дашборд */4ч/api/v2/nm-report/detail вернул 404. Найден и интегрирован новый: /api/analytics/v3/sales-funnel/products/history (до 7 дней, 20 nmIds, 3 req/min)soveshanie_bd_2026-04-01.txtdvorec_readonly настроены. Старая dvorec_analytics не тронута — все скрипты и дашборды работаютsales_wb (88 колонок, структура из wb_realization_report), sales_ozon, sales_ym, sales_ke, sales_other (с полем marketplace). Оприходование: shipments (поля include_flag, movement_type, destination). Справочники: product_chars (+ столбец duplicate_of для дублей), bom. FIFO: fifo_receipts, fifo_sales, fifo_reportcollect_daily.py использовал dateFrom=вчера в WB API /stocks, из-за чего получал только товары с изменениями за день (~500 позиций). Исправлено на dateFrom=2019-01-01 — теперь собираются ВСЕ остатки. Результат: ИП 2 469 → 8 912 шт, всего WB 8 612 → 39 938 шт, позиций 505 → 7 134monthly_fbo_stocks.py: поле quantity заменено на quantity_full (полные остатки вкл. зарезервированные). Cron сдвинут с 00:00 UTC на 05:00 UTC (08:00 МСК) — после сбора данных. Перегенерирован и отправлен в группу «КЗС Рабочая группа» с актуальными даннымиmonthly_fbo_stocks.py на VPS: 1-го числа каждого месяца (03:00 МСК) выгружает остатки WB + Ozon по всем юрлицам в Excel и отправляет в Telegram-группу «КЗС Рабочая группа». Бот @ap_degen_bot. Тест: WB 505 позиций (7 936 шт.), Ozon 6 505 позиций (98 336 шт.). Каждый файл содержит лист «Сводная» + отдельные листы по организациямapi_get() с неправильным отступом. Монитор не запускался. Исправленоmp_accounts и write-токен в WRITE_TOKENS. ИП может публиковать ответы через бота. Действителен до 28.09.2026<, >, &), ломавшие HTML-парсинг Telegram. Добавлена функция html_escape(), улучшено логирование ошибок. 120 застрявших записей переотправлены менеджерамmp_accounts + WRITE_TOKENS. Все действительны до сентября 2026. Ожидаем остальные 8 кабинетовUPPER() в 10 местах FIFO SQL. Результат: 512 → 7 192 строки, 4 → 73 SKU за неделю, COGS ИП: 62.4 млн₽ (было 2.4 млн₽)load_wb_realization_vps.py (VPS-нативный), cron каждый понедельник 05:00 UTC (08:00 МСК). Загружает reportDetailByPeriod для всех 11 кабинетов, чанки 28 дней, rate limit handling. FIFO запускается следом в 06:00 UTC — данные всегда актуальныwb_products.vendor_code или wb_realization_report.sa_name по nm_id. Поиск работает и по артикулу/v3/finance/transaction/list) и ЯМ через APIauto_published. Работает для организаций с write-токеном (Весна, бумбокс). Отзывы с рейтингом 1-3 и вопросы — всегда через менеджераreview_monitor.py и review_bot.py. Дарья может публиковать ответы по бумбоксу через ботаbuyer-chat-api.wildberries.ru позволяет только отвечать на чаты, начатые покупателем. Ограничение самого Wildberries, не ботаreview_monitor.py сверяет список неотвеченных с WB API и помечает обработанные как answered_externally. Результат первого запуска: 459 из 491 записей синхронизировано — реально ожидают ответа только 34response_templates с 10 шаблонами (6 для отзывов, 4 для вопросов). Кнопка «📋 Шаблоны» в каждом сообщении бота → список шаблонов с эмодзи по категориям → предпросмотр с подстановкой имени → публикация/редактирование. Шаблоны поддерживают плейсхолдер {имя}userName, проходит валидацию: нормальное имя (Ирина, Андрей) → обращение по имени; фамилия+имя (Филатова Светлана) → извлекается имя; пустое/цифры/спецсимволы → общий ответ без имени. Имя сохраняется в колонке customer_namecode (копируется тапом), 👁 ссылка на вкладку отзывов/вопросов карточкиbuyer-chat-api.wildberries.ru) позволяет только отвечать на чаты, начатые покупателем. Ограничение самого WBload_wb_realization_2026.py не видел JSON-файл в Docker-контейнере. Причина: SFTP загружает файл на хост VPS, а pg_read_file() читает из файловой системы контейнера. Исправление: добавлен docker cp между SFTP-загрузкой и SQL-вставкойwb_realization_report для всех 11 кабинетов за 2026 год: ИП +46 747, бумбокс +76 325, Книфелд +38 258, Олимп +32 078, Свежо +14 261, Весна +1 596, Ст +5 808 и др. Итого в таблице: 11 организаций, данные до 22.03.2026fifo_auto_update.py: найдено 9 192 новых строки (ИП WB: 4 977 после 2026-02-01, Весна WB: 4 215 после 2025-12-31). Результат: отчёт вырос с 106 до 512 строк, период расширился с 2026-03-08 до 2026-03-22. COGS ИП: 2 429 788₽generate_ads_report_vps.py был устаревшим: использовал поле shks вместо orders_associated. Обновлена SQL-агрегация, JS-логика расчёта прямого/полного CPO, таблица кампаний, summary-бар с «Всего товаров (с ассоц.)». VPS-скрипт синхронизирован с локальной версией и задеплоенwb_realization_report для ИП и Весна, т.к. данные ещё не были загружены. Пустые строки с doc_type_name = NULL — это логистика/хранение/штрафы (правильно фильтруются, не влияют на расчёт)fifo_auto_update.py (cron пн 09:00) автоматически подтягивает новые данные из wb_realization_report после каждой загрузки. Данные ИП + Весна обновляются каждую неделюrename_metabase_tables.py: 105 таблиц получили читаемые названия с категорийными префиксами (WB—, Ozon—, ЯМ—, КЭ—, Финансы—, Реклама—, FIFO—, Бот—, Справочник—, GSheet—, ИП—, Весна—, BP—, Аналитика—, Вью—). В браузере Metabase вместо wb_realization_report теперь отображается «WB — Отчёт реализации (API)»load_wb_realization_2026.py — получает данные через reportDetailByPeriod API для всех 11 кабинетов, чанки по 28 дней, дедупликация через ON CONFLICT DO NOTHING, автоопределение стартовой даты по уже загруженным данным, обработка rate limit (429 → пауза 65 сек)PATCH /api/v1/feedbacks/answer, HTTP 204 = успех. Отзывы публикуются через @packmen_bot; Дарья (@dasshkfd) работает с ИП и Веснаpg_read_file не находит файл (SFTP пишет на хост, Docker не видит /tmp хоста). Исправление применено в следующую сессиюreview_monitor.py (cron */30). 5 кнопок: Опубликовать, Редактировать, Написать свой, Пропустить, Перегенерировать. Ссылки на карточки WB в каждом сообщении/stats — полная статистика только для админаINSERT 0 1 вместо INSERT in result)collect_wb_extended_vps.py (cron 09:00, 6 таблиц), weekly_alerts.py (пн 10:00, токены+сводка+стокауты), review_monitor (*/30). Полный crontab: 12 задач на VPSPATCH /api/v1/feedbacks/answer (отзывы) и PATCH /api/v1/questions (вопросы). Старый эндпоинт PATCH /api/v1/feedbacks отключён WB. Код 204 = успехgenerate_reviews_stats_vps.py, cron каждые 6 часов. Дашборд dvorecmasterov.ru/reviews-stats/fifo_auto_update.py: автосбор WB реализации через API (reportDetailByPeriod) для ИП + Весна, обновление поступлений/BOM из Google Sheets, пересчёт FIFO. Cron каждый понедельник 09:00 МСКscripts/setup_fifo_v2.py — полный pipeline: загрузка поступлений из 3 вкладок (отгрузки с КЗС ИП, Весна, 2026) + продажи из 6 источников (WB fin_wb, Ozon ИП/Весна, ЯМ ИП/Весна, КЭ) + BOM расходниковФО для БД + 3 вкладки поступлений доступны без авторизацииdocs/plans/2026-03-14-fifo-bom.mdfin_wb — определён маппинг entity: 'ИП Галимзянова' → 'ИП', 'ООО Весна' → 'Весна'. Найден фильтр doc_type IN ('Продажа','Возврат') для исключения строк комиссийfetch_gsheet() для загрузки данных из Google Sheets через gviz API: обработка JSONP-ответа, парсинг дат Date(2026,0,1) (JS-формат с 0-based месяцами)load_receipts() — загрузка поступлений из 3 вкладок с разной структурой колонок (ИП: C/E/G/H/K, Весна: то же, 2026: A/D/F/M/R)dvorecmasterov.ru/ads-report/ — фильтры период/организация, автообновление 21:00 МСКload_sales() для FIFO v2 — загрузка продаж из 6 источников: WB из fin_wb (с маппингом entity), Ozon ИП/Весна, ЯМ ИП/Весна, КЭ ИП. Каждый источник со своей структурой колонокsold_from_layer = LEAST(qty_in, GREATEST(0, cum_out - cum_in_prev))collect_ad_stats.py — добавлен сбор бюджетов кампаний, обновление статусов. Фильтр по организациям (ИП + Весна)ad_daily_stats/root/logs/ads_report.lognms, а не nm. Теперь собирается детализация по каждому товару: 291 уникальный артикулnms, а не поле nm. Исправлен парсер в collect_ad_stats.pywb_products: конкретные смесители, душевые системы, их CPC/CR/CPOad_daily_stats — изучение структуры, проверка корректности сбораcollect_ad_stats.py на VPS, cron 3 раза в день (08/14/20 MSK). WB Advert API v3: fullstats по кампаниям. Первые 5098 строк в ad_daily_statske_salesCLAUDE.md загружает .env автоматически в каждом проекте[[AND sku_group ILIKE '%' || {{sku_filter}} || '%']] — условие необязательное, без ввода показываются все данныеscripts/add_fifo_filters.py — обновляет SQL 4 карточек и параметры дашборда через Metabase APIfifo_receipts, fifo_sku_mapping, fifo_weekly_reportsetup_fifo.py: все создаваемые карточки и дашборды теперь сразу помещаются в коллекцию «ИП + Весна» (collection_id=7)scripts/setup_fifo.py: создание 3 таблиц PostgreSQL, загрузка поступлений, расчёт FIFO, публикация дашбордаfifo_receipts — поступления товара, fifo_sku_mapping — маппинг дублирующих артикулов, fifo_weekly_report — итоговый отчёт по неделямquantity в wb_daily_sales для ИП равно NULL — каждая строка = 1 единица. Исправлено: вместо ABS(quantity) используется 1/-1 по префиксу sale_id (S = продажа, R = возврат)data/fifo_receipts_full.tsvdocs/тз расчёт себеса.docx (661 строка)sold_from_layer = LEAST(qty_in, GREATEST(0, cum_out - cum_in_prev))memory/fifo_project.mdcolumn does not existprice_with_disc в wb_daily_orders → total_price (колонка есть только в wb_daily_sales)retail_amount→retail_price, ppvz_for_pay→to_seller, delivery_rub→delivery_cost (это колонки wb_realization_report, а не fin_wb)completed, данные возвращаются корректно[[AND date_start >= {{start_date}}::date]] — фильтр опциональный (без выбора — все данные)dvorec_readonly для безопасного доступа бота к БДpostgresql-client на хосте VPS — бот выполняет psql напрямую (не через Docker)update_openclaw_identity.py — обновляет IDENTITY.md на VPS и перезапускает gatewaynext cursor с limit=1000collect_wb_new_endpoints.py для массового сбора данных на следующий деньcollect_wb_realization.py) по всем 11 кабинетам с 29.01.2024russify_metabase2.py переименовал технические колонки в русские названияmp_accounts с новыми ключами для всех обновлённых кабинетовusers.auth.php и acl.auth.php через SFTPcollect_wb_extended.py — расширенный сбор данных WB через API по всем 11 кабинетамfin_wb: проверены все 768K строк, ключевые поля и payment_reasonpayment_reason, корректный расчёт маржиgsheet_wb_realization: 58,608 строк — финансовые отчёты WB (нояб 2025 – фев 2026)gsheet_product_chars: 386 товаров с кросс-артикулами WB/Ozon/ЯМ/KE/SMMgsheet_wb_ad_stats: 13,795 строк статистики рекламных кампанийip_kzs_shipments: 4,862 отгрузки с завода (ИП) + vesna_kzs_shipments: 3,194 (Весна)ip_bank_statements: 3,815 банковских операций, ip_bank_receipts: 612 поступленийbp_* таблицыcollect_daily.py (757 строк) для сбора данных по всем 31 кабинетуmp_accounts + 6 daily-таблиц с UNIQUE-индексамиmp_accounts: 31 аккаунт (11 WB + 11 Ozon + 9 ЯМ)test_ozon_ym.py, test_ozon_stocks.py, full_run_v2.py/root/scripts//var/log/mp_collect.logapi_endpoints.md (все эндпоинты WB/Ozon/YM), обновлены db_schema.md и MEMORY.mdfin_wb (768K строк), fin_ozon, fin_ozon_upd, fin_ym, fin_ke+fin_ke_services, fin_smm