## Design Файл: `/opt/vesti/web/app.py`, функция `_fetch_candidates`. Текущее (строки ~151-165): ```python where = [] params = [] if direction: where.append("p.direction=?") params.append(direction) if status: where.append("p.status=?") params.append(status) if own in ("1", "0"): where.append("p.is_own=?") params.append(int(own)) if q: where.append("(p.text LIKE ? OR p.summary LIKE ?)") params += [f"%{q}%", f"%{q}%"] w = ("WHERE " + " AND ".join(where)) if where else "" ``` Проблемы: 1. При `status=''` (по умолчанию) в кандидаты попадают published и rejected. 2. `own=''` (по умолчанию) не фильтрует is_own → в кандидаты попадают 846 постов собственного канала (is_own=1), которые не являются кандидатами. Правка — два условия добавляются в WHERE всегда: ```python where = [] params = [] # Кандидаты = только внешние (is_own=0) посты в статусе new. # Свои посты канала (is_own=1) не являются кандидатами — это контент # собственного канала, управляется отдельно (fan-out); published/rejected — # уже решённые посты, им не место в очереди кандидатов. where.append("p.is_own=0") where.append("p.status='new'") if direction: where.append("p.direction=?") params.append(direction) if status and status != "new": # Явный фильтр статуса (rejected/published) — просмотр решённых; # для 'new' условие уже добавлено выше. where.append("p.status=?") params.append(status) if own in ("1", "0"): where.append("p.is_own=?") params.append(int(own)) if q: where.append("(p.text LIKE ? OR p.summary LIKE ?)") params += [f"%{q}%", f"%{q}%"] w = ("WHERE " + " AND ".join(where)) if where else "" ``` Примечание: `own='1'` (⭐ Свои) вернёт пустую выборку — это корректно: свои посты не кандидаты. Фильтр own оставлен для обратной совместимости (внешние = own='0' = все кандидаты). ## Верификация - `openspec validate candidates-only-external-new` — чисто. - Юнит: `python -c "import sys; sys.path.insert(0,'/opt/vesti'); import web.app as A; c=A._db(); g,co,st=A._fetch_candidates(c,'','','','','date'); print([(x['key'],len(x['posts'])) for x in g]); print('new всего постов:', sum(len(x['posts']) for x in g)); c.close()"` → все группы: только внешние посты; is_own=1 посты отсутствуют, published/rejected отсутствуют. - Живой: `GET /candidates` → в списке нет ⭐, нет «✅ Опубликованные», нет published/rejected. - Рестарт `sudo systemctl restart vesti-web`.