Files
vesti/STATUS.md
T

178 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VESTI — Статус
Обновлено: 2026-09-14 (кандидаты: сохраняем группировку после действий)
<!--
История обновлений:
2026-09-14 — кандидаты: после reject/approve/comment/reclassify/rewrite сохраняется group_by и фильтры (скрытые поля форм + _cand_back); ?status=rejected больше не даёт пустой список (жёсткое status='new' только по умолчанию)
2026-09-14 — кандидаты: в списке только внешние new-посты (is_own=0 AND status='new' в _fetch_candidates); свои посты канала (is_own=1) и published/rejected больше не показываются как кандидаты
2026-09-14 — фикс published-страницы: заголовок перекрывался fixed-top меню (padding-top 1.5rem→4.5rem в base.html); на карточках добавлен ID поста (#{{p.id}} · направление)
2026-09-14 — фикс бага: статус группы в _fetch_candidates брался от первого поста → при group_by=date группа «Ранее на этой неделе» выглядела опубликованной; теперь доминирующий по группе (new > rejected > published)
2026-09-13 — fediverse: publisher умеет постить в GoToSocial (@vesti@dedinit.ru)
2026-09-13 — publisher снова на Docker; systemd1 ожил
2026-09-12 — publisher → systemd user-юнит (костыль, пока сломан systemd1)
2026-09-11 — веб-интерфейс: 4 openspec change (CDN/htmx, markdown, даты, фильтр без CDN)
-->
## Текущее состояние
- **FEDIVERSE-ПУБЛИКАЦИЯ РАБОТАЕТ** (2026-09-13): publisher-service умеет публиковать карточки в GoToSocial (social.dedinit.ru) от имени бот-аккаунта **@vesti@dedinit.ru** (Mastodon-совместимый API). Change `gotosocial-publisher`.
- Аккаунт **vesti** создан на GtS (2026-09-13, confirmed+approved, admin=0), OAuth-приложение `vesti-publisher` (client_id=01B09QNDM9YQ2VQVSRXHCVAV6K), токен в .env (`GT_SOCIAL_ACCESS_TOKEN`, scope `read write`), канал `VESTI_GTS_CHANNELS=@vesti@dedinit.ru`.
- healthz publisher: блок `gotosocial` — url, account=vesti, token_set=true, account_ok=true.
- Реальные тесты: текст → пост, медиа → пост с картинкой (media_attachments=1), delete → 200. Посты-тесты удалены, канал чист.
- Канал в запросе: `channels: ["gt:@vesti@dedinit.ru"]` (префикс `gt:`) — fediverse; `@dedinit_vesti` — Telegram. dry_run эмулирует оба.
- Базовый конвейер (прототип) работает (далее по-прежнему): краулер, классификатор, банк статей, веб :8400.
- **Бэкфилл @dedinit ЗАВЕРШЁН** (2026-09-10): crawler/backfill_dedinit.py, fetched=1040, max_post_id=1092; в БД 846 постов is_own=1, медиа в media/media/ (исторический баг пути: БД media/<file>, файл media/media/<file>; с 2026-09-12 краулер качает в media/<file>, для старых — fallback в make_card). Скрипт оставлен для повторного дозаполнения.
- **РЕАЛЬНАЯ ПУБЛИКАЦИЯ РАБОТАЕТ**: бот-контроллер @dedinit_controller_bot (id 7765665742) → @dedinit_vesti. Токен в .env. Проверено: тестовые посты (message_id 2–4).
- **publisher-service**: **Docker-контейнер** `vesti-publisher` (docker compose -f services/publisher/docker-compose.yml, 127.0.0.1:8410, restart=unless-stopped, healthy). Compose: env из /.env, TG_PROXY=socks5://host.docker.internal:1080, маунт ../../media/media:/srv/publisher/media:ro. Старый user-юнит `vesti-publisher.service` (~/.config/systemd/user/) — **disabled, не используется** (костыль на время сломанного systemd1, порт 8410 конфликтовал бы).
- **Веб :8400**: systemd-юнит `/etc/systemd/system/vesti-web.service` (**enabled**, автостарт), uvicorn web.app:app на 0.0.0.0:8400, пароль из .env (ADMIN_PASSWORD). Доступен снаружи: https://vesti.nixg.ru.
- **Cron (Hermes, no_agent, тихие)**: vesti-crawler-all-sources (`*/30`), vesti-watchdog (`*/15`, Telegram при проблемах).
- **Бэкап**: `/opt/vesti/backup.sh` (по образцу icq/netbox) → `backups/vesti_<дата>.tar.gz` + Яндекс.Диск `/mnt/yandex-disk/backup/vesti-backups/`. Root cron `45 2 * * *` (лог /var/log/vesti-backup.log). Локально 7 дней, на ЯД 30 дней.
## Архитектура (реальная)
```
vesti-web (:8400) ──HTTP POST──▶ publisher-service (:8410) ──Bot API (SOCKS5 127.0.0.1:1080)──▶ Telegram
approve (человек) VESTI_BOT_TOKEN, VESTI_BOT_CHANNELS @dedinit_vesti (+ другие)
```
- Бот-контроллер @dedinit_controller_bot публикует во все каналы, в которые добавлен администратором.
- Каналы из конфига: VESTI_BOT_CHANNELS (сейчас @dedinit_vesti, потом несколько).
- publisher-service: POST /api/v1/publish (card {text, media?, direction, lang}, channels?), GET /healthz, GET /api/v1/views/{channel}/{mid}.
## Сделано (сессия web-ui 2026-09-11)
- [x] **Внешний доступ через Caddy**: https://vesti.nixg.ru (VPS02, /opt/caddy/Caddyfile, reverse_proxy 10.8.0.2:8400); юнит веба переведён на 0.0.0.0:8400; LE-серт выпущен
- [x] **4 OpenSpec change** (по одному на замечание пользователя), все валидны (`openspec validate`):
- `deexternalize-web-assets` — удалены unpkg (htmx 1.9.12) и jsdelivr (bootstrap 5.3.3); bootstrap локализован в web/static/bootstrap.min.css; approve/reject → обычные POST-формы. 0 внешних доменов в шаблонах.
- `web-render-markdown` — Python-Markdown (>=3.6) + Jinja2-фильтр `markdown` (XSS-safe: escape→nl2br→sane_lists); тексты кандидатов/опубликованных рендерятся
- `web-format-datetime` — фильтр `dt` → `ЧЧ:ММ ДД.ММ.ГГГГ` (candidates/published/metrics), ISO `[:16]` убран
- `web-list-filter-no-js` — фильтрация чистыми GET-формами без JS/CDN (кнопка «Применить»); ожидания unpkg нет
- [x] **Баг бандлов исправлен**: `create_bundle` возвращал абсолютный путь → в published.bundle_path лежал `/opt/vesti/bundles/...` → ссылки `/bundle//opt/...` (двойной слэш). Теперь относительный путь от bundles/ (`linux/2026-09/slug.md`), 4 строки БД обновлены, роут `/bundle/{path}` работает (HTTP 200)
- [x] **AGENT.MD** — правила проекта (все изменения через OpenSpec и т.д.)
## Сделано (сессия publisher-service)
- [x] OpenSpec change `publisher-service` — создан и ВАЛИДЕН (proposal, design, spec tg-publisher-service, tasks); `openspec validate` чисто
- [x] services/publisher/ — FastAPI-микросервис: app/{main,config,telegram,channels}.py, requirements.txt, Dockerfile, docker-compose.yml, .env.example
- [x] httpx[socks] установлен в venv (для SOCKS5-прокси к Bot API)
- [x] Токен записан в .env (VESTI_BOT_TOKEN), чтение починил (load_dotenv override + путь BASE)
- [x] Сервис запущен (venv, 127.0.0.1:8410); healthz: bot=dedinit_controller_bot, proxy=socks5://127.0.0.1:1080, channels=[@dedinit_vesti]
- [x] **Реальная публикация**: POST /api/v1/publish → ok, message_id=2 в @dedinit_vesti (проверено)
- [x] web/app.py: approve → HTTP-клиент web/publisher_client.py (publish + views), импорт publisher.bot убран
- [x] .env.example обновлён (VESTI_BOT_CHANNELS, TG_PROXY, PUBLISHER_URL)
- [x] **Docker publisher**: compose (пути ../../, extra_hosts host.docker.internal, TG_PROXY override, load_dotenv override=False), контейнер Up (healthy), healthz: bot/proxy/channels — ок (2026-09-09)
- [x] **Реальный approve через веб** (сквозной: веб → HTTP → publisher(Docker) → Telegram): посты 136, 137 → @dedinit_vesti, tg_message_id 3 и 4, бандлы и статусы записаны
- [x] **Веб-баги**: 500 на /candidates (raise RedirectResponse → HTTPException 303) и tg_message_id=0 для внешних (ключ результата — канал, не направление) — исправлены
## Сделано (сессия rich-repost-card 2026-09-12)
- [x] **OpenSpec change `rich-repost-card`** — создан и ВАЛИДЕН (`openspec validate` чисто): proposal/design/tasks/specs/tg-publisher
- [x] **Формат публикации — «богатый репост»** (publisher/card.py): карточка = [комментарий модератора] + полный текст поста + ссылка на оригинал. Служебка (📁📰👁) убрана. Текст plain (не HTML) — сырые <,>,& не искажаются
- [x] **Предпросмотр ссылки отключён** (services/publisher/app/telegram.py): sendMessage → link_preview_options={is_disabled:true}; канал больше не выглядит «агрегатором ссылок»
- [x] **Медиа правится**: main.py — если media_path существует: медиа первым (caption = первые 1000 симв., лимит 1024), полный текст отдельным сообщением; без медиа — один текст
- [x] **Вебе добавлено поле «Комментарий»** (web/app.py + candidates.html): при approve комментарий встаёт первым блоком в публикацию
- [x] **Фикс бага медиа-пути краулера** (crawler/telegram_crawler.py + backfill_dedinit.py): файл скачивался в media/media/<file>, а media_path в БД = media/<file> → publisher не находил файл, медиа молча не публиковалось. Теперь скачивание в MEDIA_DIR/<basename> (= media/<file>). Для старых постов — fallback в make_card (пробует media/ и media/media/)
- [x] telegram.py: добавлен delete_message (для отмены/чистки тестовых)
- [x] **Проверено на реальном канале**: publish ушёл в @dedinit_vesti (message_id 6), текст с <b>&amp; доставлен как есть, без превью; тестовые сообщения удалены (delete_message ok). Канал чист
- [x] **publisher временно работает как локальный uvicorn** (127.0.0.1:8410, PID 807769) — Docker недоступен из-за зависшего systemd1 на хосте (внешняя проблема, не vesti)
## Сделано (сессия gotosocial-publisher 2026-09-13)
- [x] **OpenSpec change `gotosocial-publisher`** — proposal/design/tasks/specs/gotosocial-publisher, `openspec validate` чисто
- [x] services/publisher/app/gotosocial.py — клиент GtS (httpx): post_status (format=markdown), upload_media (v2), verify_credentials, delete_status, GoToSocialError
- [x] config.py: gt_social_url/token/visibility/format/channels (+ содержимое .env, .env.example)
- [x] channels.py: resolve_channels (TG) + resolve_gotosocial_channels (fediverse из VESTI_GTS_CHANNELS или `gt:`-каналы из запроса)
- [x] main.py: `_publish_gotosocial` (upload_media → post_status; status_id в ответе), healthz-блок gotosocial, dry_run для fediverse
- [x] **Аккаунт vesti создан** на GtS (admin account create, confirmed+approved), НЕ от имени kpa39l — по запросу пользователя отдельный бот-аккаунт
- [x] **OAuth-приложение vesti-publisher** (POST /api/v1/apps, scopes write write:media); токен для vesti: вставлен в БД tokens (user_id из users, scope read write) + записан в .env
- [x] **Тесты e2e**: dry_run (fediverse), реальный текст → пост, медиа → пост с картинкой, delete → 200; посты-тесты удалены
- [x] **Фикс**: message_id=0 для GtS (id — ULID-строка) → добавлен `status_id: str`
## Сделано ранее (own-content-hub)
- [x] Миграция БД (migrate_own.py): is_own, is_own_canonical, sources.own, published.distributed_dirs
- [x] sources.yaml: +dedinit (own: true), синк
- [x] Краулер own-семантика; классификатор critical + мультинаправления; карточка с атрибуцией; веб «Свои»/бейдж/чекбоксы; бандлы origin:own
- [x] Тест e2e (dry-run): approve с dirs [linux,ai] → /published с distributed_dirs
## В работе / Следующие шаги
- [x] Бэкфилл своего канала @dedinit (задача 2.3) — **ЗАВЕРШЁН** (2026-09-10): fetched=1040, max_post_id=1092, 846 постов is_own=1 в БД
- [x] publisher: медиа при Docker-запуске — **исправлено**: compose маунтит ../../media/media:/srv/publisher/media:ro
- [x] systemd vesti-web.service + cron per-source + тихий watchdog — **юнит установлен** (подхватится при перезагрузке); **cron**: vesti-crawler-all-sources (`*/30`, тихий) + vesti-watchdog (`*/15`, Telegram при проблемах)
- [x] Классификатор по неклассифицированным — **запущен** (2026-09-10, 870 постов, фон; статус в /tmp/vesti-classify.log)
- [ ] Проверить результат классификации (в БД: direction заполнены) и выборочно качество на своих постах
- [ ] **rich-repost-card в проде**: после оживления systemd1 на хосте пересобрать Docker-контейнер publisher (docker compose -f services/publisher/docker-compose.yml up -d --build), перезапустить веб (подхватит шаблон + comment), проверить approve поста с медиа (send_photo/send_video)
- [ ] **Закрыть OpenSpec change rich-repost-card** (статус APPLIED после подтверждения), архивировать
- [x] ~~git-репозитории (gitverse истина, gitea зеркало)~~ — /opt/vesti **git-репо** (2026-09-13): gitverse kpa39l/vesti (main) + gitea-зеркало bigbox
- [ ] openspec archive web-изменений (deexternalize-web-assets, web-render-markdown, web-format-datetime, web-list-filter-no-js) и publisher-service / tg-crawler-publisher-prototype (после подтверждения)
## Как запустить / проверить
```bash
cd /opt/vesti
# publisher-service (ОСНОВНОЙ способ — Docker):
docker compose -f services/publisher/docker-compose.yml up -d --build # контейнер vesti-publisher, :8410
curl -s http://127.0.0.1:8410/healthz # ok, bot=dedinit_controller_bot, proxy=socks5://host.docker.internal:1080, channels
docker logs -f vesti-publisher # логи
curl -s -X POST http://127.0.0.1:8410/api/v1/publish \
-H 'Content-Type: application/json' \
-d '{"card":{"text":"<b>Тест</b>","direction":"linux","lang":"ru"}}'
# fediverse (GoToSocial @vesti@dedinit.ru):
curl -s -X POST http://127.0.0.1:8410/api/v1/publish \
-H 'Content-Type: application/json' \
-d '{"card":{"text":"Тест fediverse","direction":"linux"},"channels":["gt:@vesti@dedinit.ru"]}'
# dry_run (эмуляция без внешних вызовов): добавить "dry_run":true
# (старый user-юнит vesti-publisher.service disabled — не использовать, порт занят)
# веб (пароль из .env: VESTI_WEB_PASSWORD or ADMIN_PASSWORD):
# systemd-юнит: /etc/systemd/system/vesti-web.service (после daemon-reload)
# вручную: set -a; . ./.env; set +a; .venv/bin/uvicorn web.app:app --host 127.0.0.1 --port 8400
# краулер/классификатор:
.venv/bin/python -m crawler.telegram_crawler --all # инкрементально все источники
.venv/bin/python -m crawler.backfill_dedinit # бэкфилл своего канала (разовый)
CLASSIFY_TIMEOUT=20 .venv/bin/python -m classifier.classify --db db/vesti.db --limit 200
```
## Сделано (сессия закрытия 2026-09-13)
- [x] **Классификация направлений приведена к канону** `linux, tech, politics, games, electronics, llm` (AGENT.MD): keywords.py (add politics/llm, dev→tech, убрать media/ai, +golang), classify.py (промпт из DIRECTIONS_CANON + **LLM-fallback при dict-miss**: словарь не дал → всё равно LLM), web/app.py (фильтр из канона). Change `unify-directions` архивирован.
- [x] **Переклассификация**: 503 (466 своих + 37 внешних) + 206 (сброшены dev/media/ai) → 966/1019 классифицированы; без direction осталось 53 (шум/личное: музыка, поздравления — не новости). Старые мусорные dev/media/ai устранены.
- [x] **git-репозиторий** /opt/vesti: origin gitverse.ru `kpa39l/vesti` (main, private), зеркало gitea bigbox `estorozhenko/vesti` (mirror 8h). .gitignore: backups/ media/ db/ bundles/ logs/ секреты.
- [x] **Тест публикации с медиа сквозь веб**: сообщения ушли в @dedinit_vesti (11/12) и удалены. Обнаружен и исправлен фейковый `dry_run` в publisher (поле уехало в PublishResponse вместо PublishRequest; собыл 500). Change `publisher-dry-run-fix` архивирован: `dry_run=true` → эмуляция без TG.
- [x] **OpenSpec changes архивированы** (8): fix-approve-dirn, web-media-preview, web-format-datetime, web-list-filter-no-js, deexternalize-web-assets, web-render-markdown, publisher-dry-run-fix, unify-directions.
- [x] **Реализован change `candidates-mail-ui`** (двухпанельный интерфейс кандидатов как почтовый клиент): левая панель — список с группировкой (source/date/status, свертываемые <details>) + чекбоксы bulk (approve/reject/reject-old), правая — детали поста (меню действий, комментарий, черновик пересказа, оригинал с медиа). Роуты: POST /candidates/select, /candidates/bulk, /posts/{id}/comment, /posts/{id}/reclassify (qwen), /posts/{id}/rewrite (qwen). БД: добавлены `comment TEXT`, `rewritten_text TEXT`. Без JS (только данные формы). Протестировано TestClient (группировка, bulk-reject, comment, reclassify, rewrite, поиск) — зелёные. Change архивирован (2026-09-13).
## Текущие задачи (из TODO, открытые)
- Интерфейс кандидатов как почтовый клиент (2 панели, группировка, bulk) — ✅ реализован в change candidates-mail-ui (см. выше).
- Ручное указание/изменение направления (значок «?» у неклассифицированных)
- Переписывание новостей от себя (LLM-пересказ, персональный контент)
- Извлечение источников из чужих постов + сервис поиска источников в интернете (дообогащение ТОЛЬКО одобренных)
- Страница управления источниками (CRUD + фильтр по типу: Telegram, RSS, почта, сайты, Twitter)
## Сделано (сессия кандидаты-фиксы 2026-09-14)
Сессия посвящена трём багам страницы кандидатов, все исправлены через OpenSpec-процесс.
- [x] **change `candidates-only-external-new`** (коммит `acb49cc`): в списке кандидатов показывались одобренные к публикации посты и «все посты как свои».
- Причина: в `_fetch_candidates` при пустом фильтре статуса НЕ было фильтрации `p.status`, поэтому в список попадали published (6) и rejected (3); а 846 постов обратного филла своего канала @dedinit (`is_own=1`, status=new) при группировке по дате заполняли группы 04-06.2026 полностью и выглядели «своими» (⭐), хотя пользователь их не одобрял.
- Решение (по выбору пользователя, вариант B): жёстко `p.is_own=0 AND p.status='new'` — кандидаты = только внешние new-посты. Свои посты канала не кандидаты, управляются через fan-out/«Свои».
- Итог: 164→163 поста, все внешние new; группы 04-06.2026 (чисто свои) исчезли; 0 бейджей «СВОЙ».
- [x] **change `keep-candidates-context`** (коммит `0999424`): после «Отклонить» при группировке по дате открывалась пустая страница («нет кандидатов по фильтру») и фильтр сбрасывался на «источник».
- Баг 1: жёсткое `p.status='new'` конфликтовало с явным `?status=rejected` (`new AND rejected` = пусто).
- Баг 2: редирект после reject жёстко шёл на `/candidates?status=rejected` без `group_by`.
- Решение: жёсткое `new` только при пустом статусе; явный статус — единственный фильтр. Хелпер `_cand_back()` строит URL возврата с сохранением group_by/direction/own/q; во все формы правой панели (approve/reclassify/rewrite/reject/comment) добавлены скрытые поля контекста.
- Итог: `?status=rejected` показывает 4 поста (не пусто); после reject остаёшься в своей группировке.
### Итоговое состояние страницы кандидатов (после сессии)
- Список: только внешние new-посты (сейчас 163), без published/rejected и без своих.
- Группировка (date/source/status) и фильтры сохраняются после любых действий из правой панели.
- Тихие OpenSpec-change: `candidates-only-external-new`, `keep-candidates-context` — задачи [x], валидны; архивировать после подтверждения.
## Ключевые артефакты
- /opt/vesti/AGENT.MD — правила проекта (обязательно: все изменения через OpenSpec)
- /opt/vesti/openspec/changes/{deexternalize-web-assets, web-render-markdown, web-format-datetime, web-list-filter-no-js}/ — 4 change веб-интерфейса (валидны, задачи [x])
- /opt/vesti/openspec/changes/rich-repost-card/ — change формата публикации (proposal/design/tasks/specs/tg-publisher, валиден)
- /opt/vesti/services/publisher/ — микросервис (app/, Dockerfile, compose, README)
- /opt/vesti/web/publisher_client.py — HTTP-клиент веба к микросервису
- /opt/vesti/openspec/changes/publisher-service/ — OpenSpec change (proposal/design/spec/tasks)
- publisher/bot.py — старое (логика перенесена в services/publisher/app/telegram.py)
- publisher/card.py — карточка (осталась)
- db/vesti.db, sources/sources.yaml, crawler/, classifier/, web/, bundles/
## Открытые вопросы
- ~~Медиа при Docker-запуске~~ — решено: compose маунтит ../../media/media:/srv/publisher/media (веб шлёт media/<file>)
- **publisher в проде = Docker** (2026-09-13): systemd1 (D-Bus org.freedesktop.systemd1) после перезагрузки ожил, контейнер vesti-publisher (compose, restart=unless-stopped) работает healthy. User-юнит vesti-publisher.service выключен (disabled) и не используется. Если снова сломается systemd1 — вернуть юнит: systemctl --user enable --now vesti-publisher.service.
- Fan-out: сейчас все каналы из VESTI_BOT_CHANNELS (один). В будущем — направления→каналы (маппинг).
- ~~Бэкфилл канала (задача 2.3)~~ — ЗАВЕРШЁН (2026-09-10): 1040 постов, 846 в БД is_own=1
- ~~git-репозиторий /opt/vesti~~ — СДЕЛАН (2026-09-13): gitverse kpa39l/vesti (main) + gitea-зеркало bigbox (8h)
- systemd daemon-reload на bigbox зависает (внешняя проблема, не vesti) — юнит подхватится при перезагрузке
- ~~Качество классификации~~ — ПРИВЕДЕНО К КАНОНУ (2026-09-13): 966/1019 с direction; 53 без — шум/личное