Перейти к содержанию

Концепция интерфейса оператора posinus

Версия 2 от 25 июля 2026. Первая версия прошла три рецензии, они лежат рядом в docs/reviews/: новостной редактор, обычный пользователь, архитектор веб-приложений. Документ переписан по их замечаниям и служит заданием на переработку интерфейса.

Что изменилось против версии 1, коротко. Появились экраны «Эфир» и «Отбор» вместо разрозненных блоков, появился стоп-кран, правка пересказа и снятие поста. Меню ужалось с девяти пунктов до шести. Раздел про технику переписан целиком: версия 1 предлагала одновременно читать чужую базу и писать в неё, а главный риск (что случится с публикатором, когда в его базу придёт третий процесс) даже не назвала. Порядок работ переставлен.

1. Что делает система

Пять стадий, одна лента новостей проходит их по очереди.

источники -> сбор (crawler)
             оценка по 20 осям (evaluator)
             отбор по пороговому профилю (evaluator, та же транзакция)
             подготовка: пересказ и картинки (preparer)
             публикация: Telegram, wildcar.ru, ВК (publisher)

Живые числа на 25 июля 2026: около 6200 новостей, из них 120 с решением «позитивная» и 6108 с «не позитивная». Двадцать источников. Двадцать осей оценки в пяти категориях. Три площадки. Сбор идёт непрерывно, три таймера конвейера просыпаются раз в 10, 15 и 30 минут, новый пост выходит не чаще раза в два часа.

Кто пользователь

Один человек, владелец проекта. Он же оператор, он же выпускающий редактор, он же тот, кто чинит сломавшуюся площадку. С ноутбука он разбирается и калибрует, с телефона делает ровно три вещи: останавливает публикации, снимает новость с очереди, снимает вышедший пост. Беда случается, когда человек не за столом, поэтому эти три действия обязаны работать на телефоне.

Он знает систему изнутри, но интерфейс всё равно говорит по-русски, а не на языке кода. Словарь замен в разделе 10.

2. Что не так сейчас

Конвейера в интерфейсе нет вообще. Оценщик, подготовщик и публикатор живут в systemd, их состояние читается через journalctl и ручной sqlite3. Половина машины невидима.

Результата работы тоже не видно. Пересказ, картинки, ссылка на пост, статус ВК, число попыток лежат в базе конвейера и ни строчкой не показаны. Простейший вопрос «что вышло вчера» решать негде.

Список новостей отдаёт первые 200 строк без пагинации и без честного счётчика. Колонок четыре: дата, заголовок, язык, источник. Ни оценки, ни решения, ни стадии.

Двадцать осей развёрнуты сразу сорока ползунками. Настроить один порог можно, сравнить новости между собой нельзя.

Нигде не видно, почему новость отклонена, хотя профиль детерминирован и его вывод можно показать тремя строками. Калибровка отбора при этом стоит первым пунктом в pipeline/AGENTS/STATE.md.

Кроме дыр в интерфейсе рецензии нашли четыре дефекта в коде. Они чинятся до всякой переработки, потому что новый интерфейс строится ровно на этих местах.

Фильтр по решению врёт. news_list фильтрует по сырой таблице событий (review_events__decision), а не по последнему событию. Контракт исправляет решения новыми событиями, конвейер этим уже пользовался при backfill. Новость с историей «пропущена, потом не позитивная» найдётся по обоим фильтрам. Тот же перекос в дашборде.

Фильтры по осям не ограничены отборщиком. LatestEvaluationScore фильтруется без selector_name, поэтому снимок баллов, скопированный ручным отбором в событие operator:*, участвует в фильтрации наравне с машинной оценкой.

Перевод отдаёт 504. Таймаут роутера 300 секунд против proxy_read_timeout 60s в Nginx. Модель продолжает работать, ответ уходит в никуда, человек жмёт кнопку второй раз, и второе нажатие оплачивается отдельно.

Кнопка «Отобрано» отправляет новость в эфир. SELECTED_SQL в preparer.py соединяет exchange_latest_reviews и фильтрует по r.decision = 'positive', не ограничивая selector_name. Ручной отбор пишет событие positive от имени operator:<логин>. Значит нажатие ставит новость в подготовку в ближайшие 15 минут и в канал в ближайшие два часа. Обратного хода нет: событие append-only, снятия поста нет. Механика задумана правильно, а подпись на кнопке про это молчит.

3. Сценарии

Для каждого: чем управляем, что происходит по шагам, что получаем, что обязано быть видно.

3.1. Сбор новостей

Владелец сценария: crawler, worker в systemd, один процесс на базу.

Запуск worker проверяет очередь раз в минуту, источник берётся, когда пришёл его next_run_at
Управляемые параметры интервал источника (от 5 минут до недели, по умолчанию 60), задержка загрузки, включение Playwright, списки include и exclude, набор точек входа с приоритетом, статус источника, уровень доверия (новое, см. 3.9)
Промежуточные шаги выбор точки входа по каскаду, условный запрос по ETag, отсев ссылок по правилам, отсев по дате, загрузка, извлечение текста, определение языка, поиск точного дубля по хешу, поиск близкого дубля за 48 часов, сохранение вхождения, сбор внешних ссылок
Результат строка CrawlRun со статусом и четырьмя счётчиками: получено, сохранено, отклонено, ошибок; новые новости и вхождения
Что видит оператор ленту обходов с раскрытием в подробности: какие ссылки отвалились и почему, каким способом извлечён текст, сколько ушло в дубли

Одна логическая новость может прийти из пяти источников. В списке это одна строка с отметкой «и ещё 4», в карточке список всех вхождений со ссылками. Само число вхождений получает новую роль, про неё в 3.9.

3.2. Оценка и отбор

Владелец: evaluator.py, таймер раз в 10 минут.

Управляемые параметры размер пачки (25), провайдер и модель (deepseek-v4-pro), температура, потолок токенов, имя оценщика, действующий профиль отбора
Промежуточные шаги сборка промпта из справочника осей, выборка пачки неоценённых, обрезка тела до 8000 знаков, вызов модели, разбор ответа с тремя попытками, проверка всех 20 ключей и диапазона, применение профиля, запись события и 20 оценок одной транзакцией
Результат событие с решением, комментарий модели в поле причины, 20 оценок, версия оценщика с фактической моделью
Что видит оператор очередь неоценённых, темп, долю провалов разбора, тепловую карту, разбор решения профиля, комментарий модели

Разбор решения. Сначала человеческая фраза, потом таблица условий как подробности. Так просили и пользователь, и редактор:

Не прошла: мало позитива (6 из 10, нужно от 8), и ни одна сильная сторона
не дотянула до 9. Ближе всех была интересность с 7.

  Обязательные        позитивность  6   нужно от 8    не прошло
                      героизм       2   не больше 4   ок
                      кликбейт      3   не больше 4   ок
                      рекламность   0   не больше 4   ок
  Хотя бы одна из     необычность   7   нужно от 9    ближе всех
  семи сильных сторон остальные шесть ниже

3.3. Ручной отбор оператором

Кнопка в карточке. С учётом того, что она реально делает, она называется «Отправить в публикацию», а не «Отобрано». Подпись под кнопкой: «Новость выйдет в Telegram, на wildcar.ru и в ВК примерно через два часа. Отменить нельзя, снять пост можно будет только вручную».

Рядом второе, спокойное действие: «Отложить». Оно ставит закладку и никуда ничего не отправляет. Оператору нужны обе кнопки, сейчас есть только опасная.

Кнопки «Отклонить» в первой версии нет, и это осознанно. Отклонение записывает событие not_positive, а purge_rejected_content через три дня стирает текст новости. Отменить можно только новым положительным событием. «Не публиковать» и «сотри содержимое» это два разных желания, разводить их надо на две кнопки с разными последствиями, и до тех пор отклонение прекрасно делает машина сама.

3.4. Перевод на русский

Действие в карточке. Управляется переменными POSINUS_TRANSLATION_*. Результат: русский заголовок, полный перевод, короткий пересказ, идентификатор модели, время.

Сейчас это синхронный вызов внутри HTTP-запроса, и он уже сломан о шестидесятисекундный таймаут Nginx. Лечится не индикатором, а схемой: задание пишется в таблицу, страница отдаёт редирект сразу, карточка показывает состояние задания и обновляет его тем же механизмом, что и пульт. Подробности в разделе 8.

3.5. Подготовка отобранных

Владелец: preparer.py, таймер раз в 15 минут.

Управляемые параметры размер пачки (5), модель и температура, потолок токенов, user-agent, каталог для файлов, доменный стоп-лист картинок (новое)
Промежуточные шаги повторное открытие статьи с проверкой robots, разбор картинок страницы и подписей, отсев иконок и слишком больших файлов, скачивание с задержкой, запрос пересказа у модели, замена длинных тире, сборка markdown с заголовком и строкой источника
Результат запись со статусом «подготовлено» или «ошибка», заголовок и тело пересказа, набор картинок с подписями, файлы в media
Что видит оператор очередь, готовый пересказ в том виде, в каком его увидит читатель, галерею картинок с указанием домена-источника и ведущей, текст ошибки и число попыток

Правка пересказа. Модель пишет «спас троих», в оригинале четверо. Сегодня выхода два: лезть в sqlite3 или не публиковать. Нужны обычные поля для заголовка и тела, сохранение с отметкой «правлено оператором» и датой. Тонкость, которую надо отразить и в спецификации подготовщика: после ручной правки перегенерация запрещена, иначе правка молча пропадёт при первой же ошибке и возврате в очередь.

Выбор картинки. Сейчас ведущей всегда становится og:image, оператор не влияет. Для снимка агентства это претензия, а для ВК жалоба на стену. В галерее у каждой картинки видно домен и исходный адрес, есть действия «сделать ведущей», «не использовать», «загрузить свою». Доменный стоп-лист в настройках, оттуда не качаем вовсе. Заглушка канала на случай, когда годных картинок не осталось.

Ошибка подготовки не финальная: новость возвращается в очередь. Значит нужен счётчик попыток, иначе новость, падающая по кругу, останется незамеченной.

3.6. Публикация

Владелец: publisher.py, таймер раз в 30 минут, модель не вызывается.

Управляемые параметры размер пачки (1 новая за прогон), минимальный интервал между новыми (120 минут), потолок попыток (8), секреты площадок, окно публикации (новое), стоп-кран (новое)
Промежуточные шаги выбор подготовленной новости, проверка темпа и окна, рендер формата под каждую площадку, отправка, запись результата
Результат строка на каждую пару новости и площадки: статус, ссылка на пост, ошибка, число попыток; когда все включённые площадки отработали, новость становится опубликованной
Что видит оператор очередь с расчётным временем выхода, что ушло и куда, живые ссылки, какая площадка упала и с какой ошибкой, когда следующая попытка

Окно публикации. Публикатор знает одно правило: 120 минут от последней отправки. Ему всё равно, четыре утра или два дня. Пост в 03:40 по Москве это потерянный охват и отписки. Окно по умолчанию с 08:00 до 22:00, и на пульте оно объясняется словами: «окно закрыто, ближайший выход завтра в 08:10, в очереди 4». Сейчас ночная тишина и тишина из-за поломки выглядят одинаково.

Стоп-кран. Утром катастрофа или объявлен день траура, а в 11:20 из канала выходит милый котёнок. Оси этого не поймают никогда: они читают текст новости, а не мир вокруг. Кнопка «Остановить публикации» с выбором срока (на час, до конца дня, до отмены) и полем причины. Очередь копится, ничего не теряется. Работает с телефона. Технически это файл в почтовом ящике конвейера, см. 8.3.

Снятие поста. Telegram даёт удаление в течение 48 часов, ВК бессрочно, у Эгеи есть снятие с публикации. Кнопка «Снять публикацию» показывает, что получится снять, а что уже поздно. Для этого в таблице публикаций надо начать хранить идентификатор сообщения рядом со ссылкой, сейчас там только ссылка.

Коды ошибок ВК показываются с готовой подсказкой: сначала человеческая фраза, код мелким шрифтом рядом. Текст уже написан в pipeline/docs/services.md. Подсказка обязана отвечать не «что не так», а «что делать»: где лежит ключ доступа, чем его заменить, когда будет следующая попытка.

3.7. Очередь публикации

Новый сценарий, в версии 1 его не было, а редактор работает именно с ним.

Порядок выхода сейчас задаёт SQL по времени подготовки. Вечером готовы одна сильная история и три средних, средние подготовились раньше, значит они и уйдут первыми, а сильная выйдет ночью. Любой выпускающий на этом месте двигает строку вверх мышкой.

Экран очереди показывает подготовленные ровно в том порядке, в котором они выйдут, с расчётным временем у каждой строки и возрастом новости. Действия на строке: поднять, опустить, отложить до времени, снять с очереди. Порядок по умолчанию считается «силой» новости (взвешенная сумма нескольких осей), а не временем подготовки, потому что время подготовки для этого худший из возможных признаков.

Устаревание. Краулер берёт только сегодняшние статьи, дальше свежесть теряется молча: три таймера плюс один пост в два часа дают разрыв в несколько суток. Новость про вчерашнее событие, вышедшая послезавтра как свежая, читается как ошибка, особенно когда в тексте «сегодня». Возраст видимой колонкой, порог протухания в настройках, автоматическое снятие с пометкой «устарела». Плюс правило для промпта подготовщика: относительные слова заменяются на дату события.

Повтор сюжета. Дедупликация работает по тексту в окне 48 часов и ловит перепечатку. Ту же историю, рассказанную другими словами через три дня, она не ловит: это два разных NewsItem. Читатель повтор замечает мгновенно, машина нет. В карточке и в строке очереди блок «Похоже на уже опубликованное»: поиск по значимым словам заголовка среди вышедшего за 30 дней, три верхних совпадения со ссылками. Такая новость получает пометку и решается человеком.

3.8. Что вышло

Отдельная лента вышедших постов по датам, как лента в самом канале. Пользователь потратил три перехода и не нашёл ответа на вопрос «вышел ли вчерашний пост», потому что публикации были размазаны по пульту, площадкам и потоку. Результат работы системы это первое, за чем приходят, и он заслуживает своего экрана.

Строка: картинка, заголовок пересказа, время выхода, три значка площадок со ссылками на посты, отклик (см. ниже), действие «снять публикацию».

Отклик. Единственная петля обратной связи сейчас это вкус одного человека. Калибровать профиль, не зная, какие посты зашли, значит крутить ручки вслепую. ВК отдаёт просмотры, лайки и репосты по уже сохранённой ссылке простым запросом без новых зависимостей. Telegram боту просмотры канала недоступны. Колонка отклика в ленте вышедших, раздел «Лучшее за месяц», а на экране отбора самое интересное: какие оси связаны с откликом. Через пару сотен постов это перестанет быть гаданием.

3.9. Достоверность и состав ленты

Двадцать осей меряют, как новость ощущается. Ни одна не отвечает, о чём она и правда ли это. Отсюда два разных риска.

Позитивный фейк. «Мальчик собрал ядерный реактор в гараже» получит девятку по необычности и девятку по позитивности и пойдёт первым в очередь. В канале про добро такой пост стоит дороже, чем в обычном новостном: сюда приходят за доверием.

Три меры по возрастанию цены. Первая почти бесплатна: число независимых вхождений уже есть в данных. Два источника это сигнал достоверности, один малоизвестный сайт это сигнал тревоги. Поднять его в колонку потока, в карточку крупно и в условия профиля отбора. Вторая: уровень доверия у источника («проверенный», «обычный», «с осторожностью») и правило, что новость из осторожного источника не публикуется автоматически, а падает в очередь с пометкой «нужен человек». Третья: оси controversy, negativity и importance оцениваются моделью и не участвуют в профиле ни одним условием. Это дыра в профиле, а не в интерфейсе, но экран отбора обязан показывать её строкой «оси, которые оцениваются и нигде не используются».

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

Лечится темой новости из закрытого списка рубрик (животные, наука и техника, медицина, дети и школа, культура, спорт, город, люди и поступки, природа, курьёзы). Дальше колонка «Тема», фильтр по ней и блок «Состав ленты за 30 дней» с долями рубрик и долями источников. Доля источников важна отдельно: если одно агентство даёт семьдесят процентов публикаций, это уже ретрансляция, а не лента.

Тут есть контрактная тонкость, из-за которой тема не делается «одним полем в промпте». Оценщику писать некуда: контракт разрешает ему две таблицы, события и оценки, а тема это ни то ни другое. Два пути. Дешёвый: тему спрашивает подготовщик и кладёт в свою базу, тогда состав ленты считается по вышедшему, а фильтр по теме работает только для отобранных. Честный: тема едет в контракт отдельной таблицей, миграцией краулера, вместе с правкой промпта оценщика и обеих спецификаций, одним коммитом. Начинаем с дешёвого, честный держим в уме.

3.10. Политика источников

Работает сама, и поэтому её надо показывать. Новый домен приходит из внешних ссылок позитивной новости, попадает в пробный режим с лимитом в 20 статей, повышается после десяти оценок при доле извлечения от 80 процентов и доле позитива от 2 процентов. Активный источник уходит в паузу, если за 30 дней при полусотне оценок доля позитива упала ниже 2 процентов.

Оператор видит цифры, по которым принято решение, а не один статус. Плюс уровень доверия из 3.9 и доля в опубликованном.

3.11. Обслуживание и сохранность

Ежедневная копия базы краулера с проверкой целостности, семь файлов в ротации. Очистка содержимого старше 90 дней. Очистка отклонённых старше трёх дней.

Базу конвейера сегодня не копирует никто, а в ней лежат пересказы, подписи и ссылки на вышедшие посты. Потерять её означает опубликовать всё заново. Она добавляется в тот же ежедневный проход.

Первый прогон чистки отклонённых обнулит содержимое примерно 4230 новостей из 6200. Предупреждение об этом висит в блоке «Требует внимания» с кнопкой «отложить на неделю»: страшная надпись без действия только раздражает. Механизм уже есть, событие retention_rejected пишется после прохода.

Отдельно стоит решить сейчас, до того как интерфейс начнёт показывать картинки: у конвейера нет никакой чистки, медиа-каталог растёт линейно, через год там десятки тысяч файлов.

4. Модель интерфейса

Одна сущность в центре. Интерфейс строится вокруг ленты новостей, а не вокруг сервисов: сервисы это исполнители, оператору интересна судьба новости. Это главное решение версии 1, все три рецензента его поддержали, оно остаётся.

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

Третье: у каждой стадии видно вход, выход и брак. Одна и та же тройка на всех пяти стадиях.

Стадия это пара значений, а не точка на шкале. Версия 1 рисовала линейную цепочку, и это неправда. Вердиктов у новости может быть несколько: not_positive от машины и positive от оператора одновременно, причём второй реально уходит в публикацию. Подготовка и публикация живут в другой базе с двумя независимыми статусами, и новость может быть в Telegram и с ошибкой в ВК одновременно.

Значит стадия описывается так:

  • судьба по вердикту: не оценена, отклонена машиной, отобрана машиной, отобрана человеком поверх отказа;
  • продвижение: не готовилась, в очереди, ошибка подготовки с числом попыток, подготовлена, в очереди на выход, частично опубликована, опубликована.

В колонке списка показывается продвижение одним словом. Значок рядом появляется, когда машина и человек разошлись. Цепочка точек остаётся в карточке новости, где под ней есть место для подписей: в строке таблицы высотой 36 пикселей две строки текста не помещаются.

Считается это двумя дополнительными запросами на страницу из 50 строк, а не запросом на строку. Первый по базе краулера за последними вердиктами, второй по базе конвейера за статусами подготовки и публикации. Дальше словарь в Python.

5. Карта экранов

Шесть пунктов вместо девяти. Один человек не должен выбирать из десяти дверей.

Пульт        что происходит сейчас, проблемы, стоп-кран, состав ленты
Поток        лента всех новостей: фильтры, стадии, оценки, массовые действия
  Новость    карточка: оригинал, перевод, оценка, подготовка, публикация, история
Эфир         очередь на выход · вышло · площадки (три вкладки)
Источники    список, карточка с выхлопом, доверием и обоснованием статуса
Отбор        редактор профиля, проверка на корпусе, связь осей с откликом
Журнал       события, прогоны сервисов, обслуживание, копии, решения оператора

Что убрано и почему. «Прогоны» отдельным пунктом: туда заходят раз в месяц при разборе поломки, место им внутри журнала. «Площадки» отдельным пунктом: они часть эфира. «Настройки» в режиме чтения: это cat конфигурационного файла ради целого пункта меню, полезнее строчка на пульте «настройки менялись 3 дня назад», а параметры прогона приезжают из строки прогона.

6. Экраны

6.1. Пульт

┌─ Сейчас ────────────────────────────────────────────── [Остановить публикации] ┐
│                                                                                │
│  Собрано сегодня 142     Оценено 138        Отобрано 3                         │
│  ждут оценки 4           обычно 130-160     обычно 2-5                         │
│                                                                                │
│  Подготовлено 3          В очереди на выход 4      Вышло сегодня 2             │
│  одна с ошибкой          ближайший выход в 14:20   обычно 3-5                  │
│                                                                                │
└────────────────────────────────────────────────────────────────────────────────┘

┌─ Требует внимания ──────────────────────┐ ┌─ Машина ──────────────────────────┐
│ ВК не отвечает третий раз подряд        │ │ Сбор            идёт, источник Х  │
│ Нужен ключ доступа другого типа         │ │ Оценка          6 мин назад, ок   │
│ [Что делать с этой ошибкой]             │ │ Подготовка      11 мин назад, ок  │
│                                         │ │ Публикация      4 мин назад, ок   │
│ Источник «Х» на паузе: позитива 0.4%    │ │ Копия базы      сегодня 04:00, ок │
│ [Вернуть в пробный режим]               │ │ Окно выхода     открыто до 22:00  │
│                                         │ └───────────────────────────────────┘
│ Подготовка новости 6412 падает 3 раз    │
└─────────────────────────────────────────┘

┌─ Состав ленты за 30 дней ───────────────────────────────────────────────────────┐
│ животные 38%  наука 17%  люди 14%  город 11%  дети 9%  прочее 11%               │
│ РИА 41%  Хабр 18%  прочие 41%                                                   │
│ Вышло 47 постов, в среднем 1.6 в день                                           │
└─────────────────────────────────────────────────────────────────────────────────┘

Что сделано намеренно.

Полосы прогресса из версии 1 убраны. Три отрезка разной длины без подписей были самым непонятным местом всего макета: пользователь смотрел на них дольше, чем на весь остальной пульт.

Рядом с каждым числом стоит ориентир «обычно столько-то». Голое число оператор без опыта оценить не может, а «в очереди 4» и «в очереди 340» это разные новости.

Блок «Требует внимания» это список действий. Если он пуст, там одна строчка «Всё в порядке». Пустой блок лучше блока с нулями, и это единственное решение версии 1, которое похвалили все трое.

Слово «ошибка» разделено по смыслу. Ошибка обхода это «пропустили», ошибка подготовки это «повторим сами», ошибка публикации это «нужна ваша помощь». В блок внимания попадает только третье и повторяющееся второе.

Стоп-кран стоит в шапке, на самом видном месте, и он же первое, что видно с телефона.

6.2. Поток

Поиск по заголовку и тексту [                    ]

Быстрые фильтры: Нужно решить · Отобранные без подготовки · Ошибки · За вчера

Условия: [Стадия: отобрана ×] [Позитивность от 8 ×] [+ добавить условие]

Найдено 47                                                    Колонки оценок ⚙

┌──────┬───────────────────────────────┬─────────┬──────┬────────────┬──────────┐
│ Дата │ Заголовок                     │Источники│ Тема │ Стадия     │ Оценки   │
├──────┼───────────────────────────────┼─────────┼──────┼────────────┼──────────┤
│25.07 │ Кошка нашла дорогу домой      │РИА и ещё│живот-│подготовлена│ поз 9    │
│ 11:20│ переведена                    │ 2       │ные   │            │ инт 8    │
│25.07 │ Школьники собрали телескоп    │Хабр     │наука │опубликована│ поз 9    │
│ 09:05│                               │         │      │ tg вк сайт │ инт 6    │
│25.07 │ Землетрясение в ...           │AP       │      │отклонена   │ поз 2    │
│ 08:44│                               │         │      │            │ инт 1    │
└──────┴───────────────────────────────┴─────────┴──────┴────────────┴──────────┘
                                  [ Показать ещё 50 ]

Стадия словом, а не значком: в строку высотой 36 пикселей подпись под точками не влезает, а без подписи точки читаются как гадание.

Оценки с подписью оси у каждого числа. Четыре голых числа «9 2 8 7» с обрезанной подписью внизу не читаются: непонятно, что к чему относится и хорошо ли это (для позитивности девять хорошо, для негативности наоборот). Осей в колонке по умолчанию две, набор настраивается.

Число вхождений выводится словами «и ещё 2», а не «плюс 2»: знак плюс читается как оценка.

Пагинация обычная, страницами по 50, с честным счётчиком. Курсорная пагинация из версии 1 выброшена: при сортировке по имени источника ключ курсора обязан совпадать с ключом сортировки, а там вычисляемый агрегат без индекса. Сортировка по имени источника выброшена следом, потому что оператору нужен вопрос «покажи новости этого источника», а это фильтр, и он уже есть.

Условия собираются плашками через «добавить условие». Двадцать ползунков разом это витрина возможностей, а не рабочий инструмент.

Массовые действия по галочкам, пачка ограничена сотней, одна транзакция, идемпотентность как в нынешнем mark_selected. После выполнения показывается список изменённого, и он остаётся доступен из журнала: одного слова «готово» после тридцати необратимых действий мало.

Поиск по тексту, а не только по заголовку. Человек помнит «была новость про кота в Норвегии», заголовок он не помнит.

6.3. Карточка новости

Кошка нашла дорогу домой через 300 километров                          [id 6412]

●━━━━●━━━━●━━━━●━━━━○
собрана  оценена  отобрана  подготовлена  выйдет в 14:20
25.07    25.07    25.07     25.07
11:20    11:31    11:31     11:46

[Оригинал] [Перевод] [Оценка] [Подготовка] [Публикация] [История]

┌─ Оценка · news-evaluator · deepseek-v4-pro · 25.07 11:31 ──────────────────────┐
│                                                                                │
│ Прошла: позитив 9 из 10, и необычность дотянула до 9.                          │
│                                                                                │
│   Обязательные      позитивность  9  нужно от 8    ок                          │
│                     героизм       1  не больше 4   ок                          │
│                     кликбейт      2  не больше 4   ок                          │
│                     рекламность   0  не больше 4   ок                          │
│   Хотя бы одна из   необычность   9  нужно от 9    прошло                      │
│   семи сильных                                                                 │
│                                                                                │
│ Комментарий модели: «редкая история о выносливости животного»                  │
│ Источников: 3 независимых                                                      │
│                                                                                │
│ Тональность          Эмоции                    Внимание                        │
│  9 Позитивность       7 Трогательность          8 Интересность                 │
│  2 Негативность       9 Милота                  7 Неожиданность                │
│                                                 9 Необычность                  │
│                                                                                │
│ Шкала 0 [░▒▓█] 10, наведение раскрывает смысл крайних значений                 │
└────────────────────────────────────────────────────────────────────────────────┘

Тепловая карта уже сделана в news_detail.html: одиннадцать ступеней одного тона, контраст текста проверен, значение продублировано цифрой, подсказка по наведению работает. Её не трогаем, все три рецензента отметили её отдельно.

Вкладка «Подготовка»: пересказ в отрисованном виде и в режиме правки, галерея с доменами картинок и выбором ведущей, блок «похоже на уже опубликованное», техника внизу мелким.

Вкладка «Публикация»: три площадки со статусом, ссылкой, попытками, временем следующей попытки, кнопкой повтора и снятия. Предпросмотр в том виде, в каком пост увидит читатель, у всех трёх площадок, а не только у Telegram.

Панель действий справа. У каждого действия одна строка о том, что произойдёт, включая доступные: версия 1 объясняла только недоступные, а страшно как раз от доступных. «Отправить в публикацию» и «Опубликовать сейчас» разведены явно, потому что второе обходит правило двух часов.

6.4. Эфир

Три вкладки.

Очередь. Список в порядке выхода с расчётным временем, возрастом, силой, темой, пометками «нужен человек» и «похоже на вышедшее». Действия на строке: поднять, опустить, отложить, снять. Сверху состояние окна и стоп-крана.

Вышло. Лента по датам: картинка, заголовок, время, площадки со ссылками, отклик, снятие.

Площадки. Карточка на площадку: включена или нет и почему, последняя успешная отправка, очередь, ошибки за неделю человеческим языком, время следующей попытки, история «эта ошибка уже была 3 июня и прошла после замены ключа», кнопка проверки вхолостую. Подпись у кнопки: «Проверить настройки. Ничего никуда не отправится, покажем только, как выглядел бы пост». Слово --dry-run в интерфейсе не появляется.

6.5. Отбор

Действующий профиль: default            [Создать черновик]

Обязательные                          Хотя бы одно из сильных
  позитивность   от [8]                 гордость за человечество  от [9]
  героизм    не больше [4]              гордость за Россию        от [9]
  кликбейт   не больше [4]              вдохновение               от [9]
  рекламность не больше [4]             эстетика                  от [9]
  [+ ось]                               интересность              от [9]
                                        неожиданность             от [9]
                                        необычность               от [9]

Оцениваются и нигде не используются: негативность, конфликтность, важность,
трогательность, курьёзность, полезность, масштаб, запоминаемость, милота, героизм*
                                                    (* только как верхний порог)

┌─ Проверка на всех оценённых новостях (6228) ───────────────────────────────────┐
│ Действующий профиль пропускает 120, это 1.9%                                   │
│ Черновик пропустил бы 214, это 3.4%                                            │
│                                                                                │
│ Сильнее всего режет: позитивность от 8 отсекает 5840, сильная ось от 9 ещё 251 │
│                                                                                │
│ [94 новости, которые добавит черновик]   [61 почти прошедшая: не хватило       │
│                                           одного балла по одной оси]           │
│                                                                                │
│ Распределение позитивности по корпусу: гистограмма от 0 до 10                  │
└────────────────────────────────────────────────────────────────────────────────┘

Списки заголовками, а не числами. Редактору важно не сколько пропустит, а кого именно пропустит и кого зря отрежет. Список «почти прошедших» ценнее всех процентов: разбор ошибки живёт именно там. У каждой строки кнопка «должна была пройти» или «не должна была», чтобы копился размеченный человеком набор для будущей настройки.

Ниже, когда наберётся пара сотен постов: связь осей с откликом площадок.

Считается это одним пивот-запросом (по строке на новость, по колонке на ось), дальше вся арифметика в Python. Замер архитектора: 0.27 секунды на нынешних 6228 новостях, 4.8 секунды на ста тысячах. Когда упрётся, результат пивота кэшируется в памяти процесса с ключом по последнему идентификатору события.

Отдельное требование, без которого экран начнёт врать: правило отбора должно быть одно на двоих. Сейчас профиль зашит в evaluator.py, интерфейс будет считать по своей копии, и через месяц копии разъедутся. Пороги выносятся в данные, обе стороны читают одно место.

Расширение контракта под пороги

Решено 25 июля 2026: пороги переезжают в базу краулера, конвейер получает право читать их. Контракт расширяется, и это делается одним коммитом вместе с миграцией и правкой обеих спецификаций, как требует AGENTS.md.

Две таблицы на стороне краулера, обычные Django-модели с миграцией:

exchange_selection_profile
  name            имя профиля, уникальное («default»)
  is_active       ровно один профиль действующий
  revision        целое, растёт на каждое изменение порогов
  note            зачем этот профиль, словами
  created_at, updated_at

exchange_selection_bound
  profile             ссылка на профиль
  characteristic_key  ссылка на exchange_evaluation_characteristics
  kind                gate_min | gate_max | highlight_min
  value               целое от 0 до 10
  уникальность по тройке (профиль, ось, вид)

Конвейер читает один объект, вьюху exchange_active_selection_profile: имя, ревизия и строки порогов действующего профиля. Пятая позиция в списке разрешённого чтения, писать по-прежнему можно только в две таблицы обмена. Правило «клиент пишет только в две таблицы» остаётся нетронутым, и это важнее всего остального в этой затее.

Три следствия, которые надо учесть в реализации.

Откат кода назад не должен ломать оценщик. Если вьюхи нет (старая база, откаченная миграция), load_profile возвращает зашитый DEFAULT_PROFILE и пишет об этом в лог. Так обновление остаётся обратимым.

Ревизия профиля едет в историю оценок. Сейчас selector_version фиксирует фактическую модель, стало быть 0.2.0+deepseek-v4-pro. Добавляется ревизия профиля: 0.2.0+deepseek-v4-pro+default.r3. Иначе через полгода будет невозможно понять, по какому правилу принято старое решение, а таблица порогов, в отличие от событий, изменяемая.

Смена порогов не пересчитывает уже оценённое. Очередь оценщика исключает новости, у которых уже есть событие от этого имени, значит новый профиль подействует только на будущее. Это ровно тот вопрос, который задал пользователь: «а что будет с теми, кто уже в очереди». Ответ честный: ничего, и кнопка «Применить» говорит это словами, а рядом предлагает второе действие «пересчитать уже оценённые». Технически это существующий проход --backfill: он читает последние оценки, считает профиль заново и пишет исправляющее событие с полным набором оценок. Модель при этом не вызывается, значит пересчёт всего корпуса стоит секунды и ноль рублей.

6.6. Источники и журнал

Источники: список с долей позитива, долей в опубликованном, уровнем доверия и статусом. Карточка показывает цифры, по которым принято решение о паузе или повышении, а не один статус.

Журнал: события системы, прогоны сервисов вкладкой, и отдельно лента решений оператора. У каждого ручного вмешательства (сняли с очереди, остановили публикации, поправили заголовок) есть строка причины. Через месяц никто не вспомнит, почему. Дёшево и однажды спасёт.

7. Объёмы

Что Сколько сейчас Решение
Новости 6228, растёт на 140 в день страницы по 50, честный счётчик, поиск по заголовку и тексту
Оси 20 в пяти категориях две настраиваемые колонки в списке, все в карточке, условия плашками
Источники 20, потенциально сотни группировка по статусу, поиск, сортировка по доле позитива
Обходы десятки в час лента с фильтром, свёртка удачных
Вхождения до пяти на новость «и ещё 2» в списке, все ссылки в карточке
Ошибки обхода до 100 в записи прогона группировка по причине с числом, разворот по требованию
Картинки несколько на новость оригиналы, ограниченные по ширине, с отложенной загрузкой
Очередь сейчас единицы, при мягком профиле десятки сортировка по силе, протухание, снятие

Про миниатюры отдельно: в краулере нет Pillow и ничего другого для обработки картинок. Тащить зависимость ради галереи на пять штук не стоит, оригиналы с ограничением по CSS выглядят так же и стоят ноль.

Замеры списка новостей на копии схемы: нынешние 6228 дают от 0.03 до 0.17 секунды в зависимости от числа условий, честный счётчик 0.09. Цель «меньше секунды» выполняется с запасом. На ста тысячах уже от 1.2 до 2.7 секунды. Обычными индексами это не лечится: вьюха exchange_latest_reviews построена на оконной функции, и SQLite материализует её целиком на каждое условие. Бесплатное улучшение прямо сейчас: собирать несколько условий в один подзапрос с группировкой вместо отдельного Exists на каждую ось. Когда упрётся, тысяч на пятидесяти: отдельная таблица краулера с последними оценками по колонкам, обновляемая триггером. Схему exchange_* при этом трогать не надо, контракт не ломается.

8. Техника

Раздел переписан целиком. Версия 1 предлагала одновременно читать чужую базу и писать в неё, а главный риск не называла вовсе.

8.1. Публикатор нельзя трогать, пока он в таком виде

Это первое, что надо починить, и до этого интерфейсу нечего делать рядом с базой конвейера.

open_own_db в preparer.py и publisher.py не выставляет journal_mode, значит база живёт в режиме отката. В нём читатель и писатель не расходятся: открытая читающая транзакция не даёт писателю завершить запись. А в publisher.run порядок такой: сначала площадка отправляет пост, потом результат пишется в базу, причём запись стоит вне блока перехвата ошибок. Если запись упадёт на блокировке, пост уже ушёл, а следа нет. Следующий прогон через 30 минут отправит его второй раз. Дубль в Telegram, дубль в ВК, дубль на сайте, а триггер всего лишь страница интерфейса, открытая в неудачный момент.

Три обязательных пункта.

con.execute("PRAGMA journal_mode = WAL")
con.execute("PRAGMA busy_timeout = 30000")

Повторять запись результата с той же экспоненциальной задержкой, что уже написана в evaluator.write_review, а при окончательном провале валить прогон громко и оставлять файл-маркер.

Читателю в вебе давать короткий таймаут ожидания, порядка двух секунд, и ловить ошибку на уровне вьюхи: блок публикаций показывает «нет связи с базой конвейера», страница живёт дальше. Интерфейс не должен уметь мешать публикации.

8.2. Чтение базы конвейера

Читать, не писать. Соединение помечается PRAGMA query_only = ON через сигнал connection_created. Права на запись по файловой системе при этом всё равно нужны: SQLite восстанавливает журнал и создаёт служебный файл, и запрет на уровне прав даёт ложное спокойствие, а ломается через неделю после установки в случайном месте.

Права раскладывает владелец сервера через install.sh, агент системные права не трогает. Нужны: группа posinus на каталоге и файлах, бит setgid и default ACL на каталоге (иначе новые файлы уедут не в ту группу, ровно как описано для основной базы в третьем разделе docs/deployment.md), групповой доступ на самой базе и на её служебных файлах, то же самое для каталога media. Отдельным разделом в docs/deployment.md.

Файла базы может не быть: на машине разработчика его нет и не будет. Один хелпер в collector/services/, который возвращает соединение или ничего, проверяя наличие файла один раз за процесс. Вьюхи показывают заглушку вместо исключения. Если объявлять второй алиас в DATABASES, понадобятся роутер с запретом миграций, настройка тестовой базы и фикстура со схемой конвейера. Схему для этого положить отдельным файлом в pipeline/deploy/ и читать как данные с обеих сторон: файл данных это не импорт кода и границу не нарушает.

Соблазн соединить две базы через ATTACH в одном запросе есть, и для сложной сортировки по стадии он единственный путь. Не как основной вариант: без явного режима только для чтения он молча создаёт пустой файл, если базы нет, и дальше пути расходятся навсегда.

Картинки отдаются вьюхой с проверкой входа, которая возвращает внутреннее перенаправление для Nginx. Файл читает Nginx, права проверяет Django, поток не идёт через Python. Раздавать приватный каталог напрямую нельзя: он окажется открыт всему интернету по прямой ссылке.

8.3. Почтовый ящик вместо таблицы заявок

Таблица заявок из версии 1 требовала записи в чужую базу, давала задержку до 30 минут и заводила второй механизм фоновых задач. Кнопка, после которой десять минут ничего не происходит, хуже отсутствия кнопки.

Вместо этого каталог /var/lib/posinus/pipeline/requests с правом записи для группы posinus. Веб создаёт в нём файл, systemd поднимает нужный сервис немедленно через .path unit. Задержка секунды, sudo не нужен, схема не меняется. Три unit-файла в pipeline/deploy/install.sh, который и так запускает владелец. Файл-заявку скрипт удаляет до начала работы, иначе получится вечный цикл; повторный запуск работающего сервиса systemd игнорирует, так что двойное нажатие безопасно.

Тот же ящик держит стоп-кран: файл pause со временем окончания и причиной внутри, публикатор читает его в начале прогона. Механизм один, применений два.

8.4. Прогоны сервисов

Экран «что делала машина» строится на строке прогона, которую сейчас никто не пишет. Оговорка к версии 1: это не тридцать строк на скрипт. У evaluator.py вообще нет своей базы, он открывает только базу краулера. Ему нужны второе соединение, схема и её миграция, вместе с тестами ближе к сотне строк, и это меняет pipeline/AGENTS/SPEC.md.

Строка пишется в начале и закрывается в конце. Если процесс убит или машина перезагрузилась, строка навсегда останется в состоянии «выполняется»: правило простое, незакрытый прогон старше двойного интервала таймера считается прерванным, интерфейс так его и показывает. Схема добавляется тем же способом, что уже используется в конвейере, и обязана переживать откат кода назад.

В строку кроме счётчиков пишется действующая конфигурация прогона без секретов: модель, размер пачки, список включённых площадок. Тогда «Настройки» не нужны как экран, а веб-процессу незачем читать /etc/posinus/pipeline.env, где лежат токены Telegram, ВК и пароль от сайта.

8.5. Долгие задачи

Перевод уходит из HTTP-запроса. Маленькая таблица заданий в базе краулера обычной миграцией, кнопка создаёт строку и сразу отдаёт редирект, карточка показывает состояние. Выполняет демон-поток в веб-процессе: Waitress многопоточный, задание одно за раз, на старте зависшие строки помечаются прерванными. Запасной вариант, если окажется мало, это существующий worker, который и так спит по минуте между источниками.

proxy_read_timeout в конфиге Nginx поднять до 120 секунд независимо от всего остального: он режет любую тяжёлую страницу.

8.6. Обновление фрагментов

Двадцать строк на голом fetch, Django отдаёт отдельный адрес с куском разметки, скрипт подменяет содержимое блока. Никакой сборки, никакого SPA. Деплой это git pull и рестарт, и это надо беречь.

Интервал 60 секунд, не 30: самый быстрый таймер просыпается раз в 10 минут, чаще опрашивать нечего. Четыре подводных камня, которые обычно находят уже в проде. Истёкшая сессия отдаёт перенаправление на вход, и форма логина въезжает внутрь блока: фрагмент отвечает кодом 401 и скрипт останавливается. Запросы накладываются, если сервер медленнее интервала: нужен флаг «запрос в полёте». Вкладка висит в фоне сутками: проверять видимость документа. Каждое обновление гоняет полный набор агрегатов: отдавать метку версии и возвращать «не изменилось».

8.7. Уведомления

Самая частая претензия пользователя: система никого ни о чём не сообщает, о поломке узнаёшь, только если сам зашёл. Телеграм-бот в проекте уже есть, отправка сообщения стоит десять строк.

Два вида. Ежедневная сводка одной фразой: «вчера вышло 2 поста, проблем нет». И аварийные: площадка упала три раза подряд, очередь пуста третий день, ни одной публикации за сутки в открытом окне. Пустой канал это тоже редакционная проблема, и кричать про неё надо так же громко, как про упавшую площадку.

Порог у аварийных высокий. Шум обесценивает канал за неделю.

9. Визуальный язык

Основа заложена в base.html, менять её целиком не надо.

Цвета. Тёмно-синий #17324d для шапки, бирюзовый #176b87 для действий, серо-голубой фон #f4f7f9. Статусы: серый это ждёт, синий в работе, зелёный #197043 готово, красный #a32b2b ошибка, жёлтый пауза. Одиннадцатиступенчатая тепловая шкала остаётся как есть.

Типографика. Системный шрифт, основной размер 14 пикселей, в плотных таблицах 13. Числа моноширинными цифрами.

Сетка. Максимальная ширина растёт с 1180 до 1440. В карточке новости колонка текста при этом ограничивается примерно 75 знаками в строке, остальное отдаётся панели действий: длинная строка нечитаема.

Плотность. Строка таблицы 36 пикселей. Семь колонок в потоке это предел, восьмую добавлять некуда, поэтому колонки оценок настраиваются, а не копятся.

Иконки только там, где символ однозначен. Эмодзи в интерфейсе нет.

Подсказки по наведению нужны везде, где стоит непривычное слово: «очередь», «попытки», «профиль», «доля позитива». У названий осей в карточке новости они уже есть и работают.

Доступность. Видимый фокус, текстовая подпись у каждого цветного статуса, значение цифрой внутри тепловой клетки, заголовки у таблиц.

Про gettext. В версии 1 было требование обернуть все строки ради будущей локализации. В настройках нет ни middleware, ни каталога локалей, пользователь один, второго языка не предвидится. Шестьсот строк в обёртках это неделя работы и налог на каждое изменение. Выброшено в отложенное.

10. Словарь интерфейса

Слова из документации, которые не должны попасть на экран. Список собран пользователем, который честно выписал всё, обо что споткнулся.

Не писать Писать
append-only записи не удаляются, поправка добавляется отдельной строкой
идемпотентно нажать можно сколько угодно раз, сработает один
детерминирован всегда даёт один ответ на одни и те же оценки
курсор, пагинация, фрагменты ничего не писать, просто кнопка «Показать ещё 50»
чипсы плашки условий, а на экране просто плашка с крестиком
пресеты быстрые фильтры
корпус все уже оценённые новости
выхлоп источника доля позитивных новостей от источника
техническое надгробие пустая карточка без текста, чтобы не потерять историю
dry-run проверка вхолостую, ничего не отправляется
токен ключ доступа, и обязательно где он лежит
код 214 человеческая фраза, код мелким шрифтом рядом
конфиг настройки
потолок токенов, температура, tier не показывать вовсе
markdown показать готовый вид
SHA-256, SimHash проверка на точный повтор, проверка на похожий текст
ETag спрашиваем сайт, изменилось ли что-нибудь
Trafilatura, Playwright программа, которая вытаскивает текст; открываем как настоящий браузер
регулярные выражения, include, exclude какие ссылки брать, какие пропускать, с примером
systemd, worker, таймер, journalctl «сбор идёт», «оценка была 6 минут назад»
оси, яркие оси требования и сильные стороны
воронка сколько дошло до конца из собранных
прогоны что делала машина
гейт одобрения проверять посты перед выходом

11. Что считать успехом

Оператор отвечает на вопрос «почему эта новость не прошла» за один клик из списка. Сейчас не отвечает вообще.

Оператор отвечает на «что вышло вчера» одним переходом в меню.

Оператор узнаёт о поломке из сообщения, а не потому, что случайно зашёл.

Ни один обычный вопрос не требует journalctl или sqlite3.

Путь новости прослеживается от адреса источника до ссылки на пост в канале и обратно до кнопки «снять».

Список из 6228 новостей открывается меньше чем за секунду и показывает честное число.

Ни одного дубля в канале из-за того, что кто-то открыл страницу интерфейса.

12. Порядок работ

Против версии 1 порядок переставлен. Там первым шёл самый рискованный кусок, который ничего не показывает на экране, а калибровка стояла четвёртой, хотя она дешевле втрое и закрывает главную открытую задачу проекта.

Шаг 0. Починить сломанное. День работы, без зависимостей, сразу заметно. Фильтр по решению через exchange_latest_reviews. Имя отборщика в фильтрах по осям. Обычная пагинация с честным счётчиком вместо среза на 200. proxy_read_timeout до 120 секунд. Кнопка «Отобрано» переименована в «Отправить в публикацию» и получает предупреждение о том, что делает на самом деле.

Шаг 1. Предохранители. Публикатор переводится в WAL, запись результата повторяется, почтовый ящик и path units раскладывает владелец через install.sh. Стоп-кран и окно публикации. Это защита от самой дорогой ошибки и одновременно фундамент для ручного запуска.

Шаг 2. Отбор и калибровка. Пороги профиля переезжают в базу краулера двумя таблицами и одной вьюхой для конвейера, у оценщика и интерфейса становится одно правило на двоих. Это расширение контракта: миграция, правка docs/contracts/database-contract.md, обеих спецификаций и load_profile в оценщике едут одним коммитом. Дальше разбор решения в карточке, экран отбора с проверкой на корпусе, списки «добавит черновик» и «почти прошедшие», кнопка пересчёта уже оценённых через существующий --backfill. Работает целиком на базе краулера, новых прав на файловую систему не нужно. Закрывает задачу номер один из pipeline/AGENTS/STATE.md.

Шаг 3. Фундамент второй базы. Права и ACL через install.sh, соединение только для чтения, хелпер, переживающий отсутствие файла, резервная копия базы конвейера, таблица прогонов во всех трёх скриптах.

Шаг 4. Пульт и эфир. Состояние машины, блок внимания, состав ленты, очередь с управлением порядком, лента вышедшего, площадки. Уведомления в Telegram. Обновление фрагмента раз в минуту.

Шаг 5. Поток и карточка. Колонка стадии по двузначной модели, настраиваемые колонки оценок, тема, число независимых источников, вкладки подготовки и публикации, правка пересказа, выбор картинки, фоновые задания для перевода, поиск по тексту.

Шаг 6. Остальное. Снятие публикации (сначала хранение идентификаторов сообщений), отклик из ВК, блок «похоже на уже опубликованное», массовые действия, уровень доверия у источников.

Выброшено совсем: курсорная пагинация, сортировка по имени источника, gettext, экран настроек, генерация миниатюр, стоимость в рублях (токены показываем, они точные, а цены deepseek-v4-pro в реестре скопированы с прежней модели и врут в неизвестную сторону). Матрица оценок 20 на 20 остаётся как разовый диагностический инструмент в глубине, без места в плане: ту же задачу решают гистограммы на экране отбора.

13. Что решено по вопросам версии 1

Гейт ручного одобрения не нужен, автомат правильный. Нужны правка текста, снятие поста и стоп-кран, и это другое.

Кнопка «Отклонить» в первой версии не делается. Отклонение запускает трёхдневную чистку текста, а «не публиковать» и «стереть» это разные желания.

Мобильный умеет три вещи: остановить публикации, снять новость с очереди, снять пост. Просмотр без них бесполезен, полная адаптация таблицы на 20 осей не окупится.

Стоимость обращений к модели показываем в токенах, деньги вернём, когда в реестре появятся настоящие цены.

Стёртые чисткой новости показываем отдельным фильтром, не смешивая с живыми.

Второй человек и роли сейчас не нужны.

Пороги профиля живут в базе краулера, конвейер читает их через новую вьюху. Владелец разрешил расширить контракт 25 июля 2026, устройство описано в 6.5.

14. Открытые вопросы

Сколько постов в день считается нормой? От этого числа считаются расписание, длина очереди и порог протухания, а в системе его нет. Ориентир редактора: от трёх до пяти в день в окне с восьми до двадцати двух.

Тема новости: дешёвый путь через подготовщика или честный через контракт? Дешёвый закрывает состав ленты, честный закрывает ещё и фильтр по всему корпусу.

Жёсткое правило против повторов рубрики («не два подряд из одной») или только показ состава и доверие человеку?

Готов ли владелец хранить идентификаторы сообщений Telegram и ВК ради возможности удалить пост? Без них снятие не работает, сейчас в таблице публикаций лежит только ссылка.

Сколько живёт база конвейера? У краулера есть чистка на 90 и на 3 дня, у конвейера никакой, медиа-каталог растёт линейно. Решить стоит до того, как интерфейс начнёт показывать картинки.