mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 09:55:03 +00:00
Session 2026-09-13: unify directions (canon+LLM-fallback, 966/1019), publisher dry_run fix, git repo, archive 8 changes, candidates-mail-ui WIP, docs
This commit is contained in:
@@ -97,6 +97,21 @@ curl -s -X POST http://127.0.0.1:8410/api/v1/publish \
|
|||||||
CLASSIFY_TIMEOUT=20 .venv/bin/python -m classifier.classify --db db/vesti.db --limit 200
|
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`** (двухпанельный интерфейс кандидатов, готовится; proposal/design/tasks/spec — валиден).
|
||||||
|
|
||||||
|
## Текущие задачи (из TODO, открытые)
|
||||||
|
- Интерфейс кандидатов как почтовый клиент (2 панели, группировка, bulk) — change candidates-mail-ui (проработать с пользователем)
|
||||||
|
- Ручное указание/изменение направления (значок «?» у неклассифицированных)
|
||||||
|
- Переписывание новостей от себя (LLM-пересказ, персональный контент)
|
||||||
|
- Извлечение источников из чужих постов + сервис поиска источников в интернете (дообогащение ТОЛЬКО одобренных)
|
||||||
|
- Страница управления источниками (CRUD + фильтр по типу: Telegram, RSS, почта, сайты, Twitter)
|
||||||
|
|
||||||
## Ключевые артефакты
|
## Ключевые артефакты
|
||||||
- /opt/vesti/AGENT.MD — правила проекта (обязательно: все изменения через OpenSpec)
|
- /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/{deexternalize-web-assets, web-render-markdown, web-format-datetime, web-list-filter-no-js}/ — 4 change веб-интерфейса (валидны, задачи [x])
|
||||||
@@ -113,6 +128,6 @@ CLASSIFY_TIMEOUT=20 .venv/bin/python -m classifier.classify --db db/vesti.db --l
|
|||||||
- **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.
|
- **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 (один). В будущем — направления→каналы (маппинг).
|
- Fan-out: сейчас все каналы из VESTI_BOT_CHANNELS (один). В будущем — направления→каналы (маппинг).
|
||||||
- ~~Бэкфилл канала (задача 2.3)~~ — ЗАВЕРШЁН (2026-09-10): 1040 постов, 846 в БД is_own=1
|
- ~~Бэкфилл канала (задача 2.3)~~ — ЗАВЕРШЁН (2026-09-10): 1040 постов, 846 в БД is_own=1
|
||||||
- git-репозиторий /opt/vesti — инициализировать?
|
- ~~git-репозиторий /opt/vesti~~ — СДЕЛАН (2026-09-13): gitverse kpa39l/vesti (main) + gitea-зеркало bigbox (8h)
|
||||||
- systemd daemon-reload на bigbox зависает (внешняя проблема, не vesti) — юнит подхватится при перезагрузке
|
- systemd daemon-reload на bigbox зависает (внешняя проблема, не vesti) — юнит подхватится при перезагрузке
|
||||||
- Качество классификации своих постов (846, словари vs LLM) — проверить выборочно
|
- ~~Качество классификации~~ — ПРИВЕДЕНО К КАНОНУ (2026-09-13): 966/1019 с direction; 53 без — шум/личное
|
||||||
@@ -54,3 +54,8 @@
|
|||||||
| 2026-09-13 | approve → 500 (UnboundLocalError 'dirn' в web/app.py) | ✅ закрыта | 2026-09-13 (change fix-approve-dirn; dirn/lang перенесены до использования; 303 вместо 500) |
|
| 2026-09-13 | approve → 500 (UnboundLocalError 'dirn' в web/app.py) | ✅ закрыта | 2026-09-13 (change fix-approve-dirn; dirn/lang перенесены до использования; 303 вместо 500) |
|
||||||
| 2026-09-13 | Медиа в карточках новостей не отображаются | ✅ закрыта | 2026-09-13 (change web-media-preview; роут /media/{filename} + превью img/video в candidates/published; клик → полноразмер в новой вкладке) |
|
| 2026-09-13 | Медиа в карточках новостей не отображаются | ✅ закрыта | 2026-09-13 (change web-media-preview; роут /media/{filename} + превью img/video в candidates/published; клик → полноразмер в новой вкладке) |
|
||||||
| 2026-09-13 | Пост без классификации: на карточке значок «?» — понять его роль; если классификации нет — дать возможность вручную указывать/изменять/добавлять направление при анализе | 🔵 открыта | |
|
| 2026-09-13 | Пост без классификации: на карточке значок «?» — понять его роль; если классификации нет — дать возможность вручную указывать/изменять/добавлять направление при анализе | 🔵 открыта | |
|
||||||
|
| 2026-09-13 | git-репозиторий /opt/vesti (gitverse истина, gitea зеркало) | ✅ закрыта | 2026-09-13 (git init + commit; gitverse kpa39l/vesti main; gitea estorozhenko/vesti mirror 8h; .gitignore: backups/media/db/bundles/logs исключены) |
|
||||||
|
| 2026-09-13 | Интерфейс кандидатов: разделить окно на 2 панели — слева список кандидатов, справа меню действий с постом (опубликовать / отклонить / переклассифицировать / переписать новость под себя), ниже блок для моего комментария, потом оригинальный пост со всеми медиа | 🔵 открыта | (зафиксирована, проработать подробности) |
|
||||||
|
| 2026-09-13 | Контент-стратегия: не перепубликовать чужие новости, а публиковать переписанные от себя (персональный контент, добавление ценности). Связано с «переписать новость под себя» в интерфейсе кандидатов — нужна генерация пересказа через LLM + редактирование перед публикацией | 🔵 открыта | (идея, проработать) |
|
||||||
|
| 2026-09-13 | Извлечение источников новостей из полученных постов (каналы/сайты, откуда пришли новости) — чтобы в следующий раз забирать новости напрямую из источника. ДОПОЛНЕНИЕ: возможно стоит разработать сервис поиска источников в интернете (если источник не указан) и ДООБОГАЩАТЬ карточки кандидатов, но — чтобы не тратить лишние деньги — ТОЛЬКО для одобренных кандидатов. Пайплайн: отбор новостей из списка кандидатов → в «выбранные» → обработка/дообогащение/переписывание при необходимости → публикация | 🔵 открыта | (зафиксирована, проработать) |
|
||||||
|
| 2026-09-13 | Отдельная страница управления источниками новостей: отображение / добавление / удаление / редактирование, фильтр по типу источника (Telegram, RSS, почтовая рассылка, прямые новости с сайтов, Twitter и т.п.) | 🔵 открыта | (зафиксирована, проработать) |
|
||||||
@@ -401,3 +401,41 @@ sudo systemctl restart vesti-web # active; login → 200, candidates/published
|
|||||||
2. Тест публикаци с медиа сквозь веб (approve с media_path → publisher send_photo) — если ещё не проверен.
|
2. Тест публикаци с медиа сквозь веб (approve с media_path → publisher send_photo) — если ещё не проверен.
|
||||||
3. git-репозиторий /opt/vesti (gitverse истина, gitea зеркало).
|
3. git-репозиторий /opt/vesti (gitverse истина, gitea зеркало).
|
||||||
4. openspec archive: fix-approve-dirn, web-media-preview, rich-repost-card + старые changes (после подтверждения).
|
4. openspec archive: fix-approve-dirn, web-media-preview, rich-repost-card + старые changes (после подтверждения).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сессия 2026-09-13 (закрытие проекта vezde + новые фичи)
|
||||||
|
|
||||||
|
### 1. Унификация направлений классификации (change unify-directions, архивирован)
|
||||||
|
Проблема: список направлений разъехался по 4 местам (classify.py:87 промпт, web/app.py:130 фильтр, keywords.py словари, канон AGENT.MD).
|
||||||
|
- Канон: **linux, tech, politics, games, electronics, llm**.
|
||||||
|
- keywords.py: `DIRECTIONS_CANON` + словари приведены (dev→tech, ai→llm, media удалён, politics добавлен).
|
||||||
|
- classify.py: промпт LLM из `DIRECTIONS_CANON`; импорт `find_direction, DIRECTIONS_CANON`.
|
||||||
|
- **LLM-fallback**: раньше LLM звалась ТОЛЬКО если словарь дал direction; при dict-miss пост оставался серым навсегда. Теперь при dict-miss тоже зовём LLM (проверка канона). Это убрало 449 серых постов.
|
||||||
|
- Убраны однобуквенные ключи `"c"`/`"go"` (шумят: `\bc\b` матчится на любую `c`). `qt/vr/ии/ai/ml` — валидны (границы слов).
|
||||||
|
- web/app.py: фильтр направлений = канон.
|
||||||
|
- **Питфол**: keywords.py перезаписан write_file → обрезался (117→99 строк, потерян хвост find_directions_all/find_direction/__main__). Восстанавливать по контракту из classify.py (find_direction→str|None, find_directions_all→list[str]).
|
||||||
|
- Переклассификация: 503 (466 своих+37 внешних) + 206 сброшенных (dev/media/ai→classified=0) → **966/1019** классифицированы. Осталось 53 без direction — шум/личное (музыка, поздравления), не новости.
|
||||||
|
|
||||||
|
### 2. Тест публикации с медиа + фикс dry_run (change publisher-dry-run-fix, архивирован)
|
||||||
|
- Тест: `POST /api/v1/publish` с dry_run:true + media → сообщения РЕАЛЬНО ушли в @dedinit_vesti (message_id 11=медиа, 12=текст). **dry_run был мёртвой опцией** (поле в PublishResponse, а не PublishRequest).
|
||||||
|
- Исправлено: `dry_run: bool = False` в PublishRequest; в роуте — ветка «эмулировать результат» (message_id=0, dry_run=true) ДО цикла по каналам.
|
||||||
|
- **Питфол**: после первой пересборки — 500 (AttributeError: dry_run в PublishResponse). Поле добавлено в PublishRequest → работает: dry_run=true → {ok, message_id:0, dry_run:true}, в TG ничего не уходит.
|
||||||
|
- Тестовые сообщения удалены через `delete_message('@dedinit_vesti', 11/12)` (в publisher есть такой метод).
|
||||||
|
|
||||||
|
### 3. git-репозиторий (задача «git репозиторий?»)
|
||||||
|
- `git init -b main`, .gitignore (backups/ media/ db/ bundles/ logs/ секреты), первый коммит c3f59f7.
|
||||||
|
- gitverse: `POST $GITVERSE_API/user/repos` (Bearer PAT + Accept `application/vnd.gitverse.object+json;version=1`, auto_init:false) → kpa39l/vesti (id 334405). Remote: `https://kpa39l:<PAT>@gitverse.ru/kpa39l/vesti.git`.
|
||||||
|
- Gitea-зеркало bigbox: `POST :3000/api/v1/repos/migrate` (clone_addr `https://oauth2:<PAT>@gitverse.ru/...`, mirror:true, mirror_interval:8h) → estorozhenko/vesti.
|
||||||
|
- **Питфол**: первый migrate таймаутнул (180с — gitea клонировал) и оставил пустой repo с mirror=True но без данных → «Repository is not a mirror» на mirror-sync. Удалить (DELETE) и пересоздать через migrate заново (дождаться 201 — миграция синхронизирует при создании).
|
||||||
|
|
||||||
|
### 4. Классификация: задачи и термины
|
||||||
|
- Новые задачи в TODO (5 открытых): интерфейс кандидатов (почтовый клиент), ручное направление («?» = direction пустой — см. candidates.html:41), переписывание от себя (LLM-пересказ), извлечение/поиск источников (дообогащение только одобренных), страница управления источниками (CRUD+фильтр типов).
|
||||||
|
- Change **candidates-mail-ui** (готовится): 2 панели (список слева + детально справа), группировка по source/date/status, bulk approve/reject, comment, reclassify, rewrite, медиа. proposal/design/tasks/spec — валидны.
|
||||||
|
|
||||||
|
### Следующая сессия
|
||||||
|
1. Проработать детали **candidates-mail-ui** с пользователем (открытые вопросы: комментарий — поле/таблица?, переписывание — qwen черновик?, переклассификация — перезапуск?, группировка — одна/несколько) → реализовать.
|
||||||
|
2. Ручное указание направления (для 53 без direction + «?» на карточке).
|
||||||
|
3. Переписывание новостей от себя (LLM-пересказ) + пайплайн «выбранные → дообогащение → публикация».
|
||||||
|
4. Извлечение источников из постов + сервис поиска в интернете (только одобренные).
|
||||||
|
5. Страница управления источниками (CRUD + фильтр по типу).
|
||||||
|
|||||||
+4
-4
@@ -6,7 +6,7 @@
|
|||||||
- [x] classify.py:87: промпт → DIRECTIONS_CANON
|
- [x] classify.py:87: промпт → DIRECTIONS_CANON
|
||||||
- [x] classify.py: LLM-fallback при dict-miss (словарь не дал → LLM, проверка канона) — убирает серые посты
|
- [x] classify.py: LLM-fallback при dict-miss (словарь не дал → LLM, проверка канона) — убирает серые посты
|
||||||
- [x] web/app.py:130: directions → DIRECTIONS_CANON
|
- [x] web/app.py:130: directions → DIRECTIONS_CANON
|
||||||
- [ ] Переклассификация 466 своих без direction (limit 1000)
|
- [x] Переклассификация 466 своих без direction (limit 1000) — 503+206 постов, LLM-fallback добор
|
||||||
- [ ] Проверка БД: нет новых направлений вне канона
|
- [x] Проверка БД: нет новых направлений вне канона (dev/media/ai устранены; осталось 53 без direction — шум/личное, не новости)
|
||||||
- [ ] `openspec validate unify-directions` — чисто
|
- [x] `openspec validate unify-directions` — чисто
|
||||||
- [ ] Бэкап после правки
|
- [x] Бэкап после правки — `/tmp/vesti.db.bak-unify` + будет полный бэкап
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-09-13
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# design.md — candidates-mail-ui
|
||||||
|
|
||||||
|
## Текущая структура (из чтения кода)
|
||||||
|
|
||||||
|
- `web/app.py` (FastAPI) — роуты:
|
||||||
|
- `GET /candidates` — рендер `candidates.html` (список карточек).
|
||||||
|
- `POST /posts/{id}/approve` — апрув (публикация через publisher).
|
||||||
|
- `POST /posts/{id}/reject` (или аналогичный) — отклонение.
|
||||||
|
- GET /media/{filename} — медиа (добавлен в web-media-preview).
|
||||||
|
- Таблица `posts`: id, source_id (FK), text, direction, lang, views, reactions,
|
||||||
|
media_path, status (new/published/rejected?), classified, created_at.
|
||||||
|
- Таблица `classifications`: post_id, direction, relevance, interest, summary, model.
|
||||||
|
|
||||||
|
## Архитектура нового UI
|
||||||
|
|
||||||
|
### 1. Левая панель: список + группировка + bulk
|
||||||
|
|
||||||
|
- Один маршрут `GET /candidates` с query-параметрами:
|
||||||
|
- `group_by` ∈ {source, date, status} (по умолчанию source)
|
||||||
|
- `filter` (статус: new/published/rejected/all)
|
||||||
|
- `q` (поиск по тексту)
|
||||||
|
- Ответ: тот же HTML (серверный рендер, Jinja2), но layout — две колонки
|
||||||
|
(Bootstrap 5 grid, уже подключён).
|
||||||
|
- Список строится из постов (new/rejected), сгруппированных по выбранному
|
||||||
|
признаку. Группы — сворачиваемые (`<details>`/`<summary>` — без JS).
|
||||||
|
- Каждый пост имеет чекбокс `<input type="checkbox" name="ids" value="{{ id }}">`.
|
||||||
|
- Форма bulk: `POST /candidates/bulk` с `ids[]` и `action` (approve/reject/reject-old).
|
||||||
|
|
||||||
|
### 2. Правая панель: детальный просмотр + действия
|
||||||
|
|
||||||
|
- Выбранный пост: `GET /candidates/{id}` (или та же страница с `?selected=<id>`).
|
||||||
|
- Секции:
|
||||||
|
- Кнопки действий: Опубликовать, Отклонить, Переклассифицировать,
|
||||||
|
Переписать новость (LLM).
|
||||||
|
- «Мой комментарий»: `<textarea name="comment">` + `POST /posts/{id}/comment`.
|
||||||
|
- Оригинальный пост: text (markdown), media (`/media/<basename>`), мета
|
||||||
|
(source @channel, канал, время, views/reactions), direction badge.
|
||||||
|
|
||||||
|
### 3. Новые роуты/данные
|
||||||
|
|
||||||
|
- `posts.comment TEXT` (nullable) — комментарий пользователя к посту.
|
||||||
|
- `POST /posts/{id}/comment` — сохранить комментарий.
|
||||||
|
- `POST /candidates/bulk` — bulk-одобрение/отклонение (транзакция).
|
||||||
|
- `POST /posts/{id}/reclassify` — перезапуск классификатора (classifier.classify_text
|
||||||
|
на посте, обновить direction/classifications).
|
||||||
|
- `POST /posts/{id}/rewrite` — LLM-пересказ (qwen3:8b через Ollama): сгенерировать
|
||||||
|
черновик текста от себя (без копипасты), сохранить в `posts.rewritten_text`,
|
||||||
|
вернуть в текстовое поле для редактирования.
|
||||||
|
|
||||||
|
### 4. Минимум JS
|
||||||
|
|
||||||
|
- Без JS: чекбоксы + `form` с method=post (multi-select через чекбоксы в одной форме).
|
||||||
|
- Группировка/сворачивание: `<details>`.
|
||||||
|
- Автоподсветка выбранного: сервер рендерит `class="active"` по `selected` id.
|
||||||
|
|
||||||
|
## Реализация (по шагам)
|
||||||
|
|
||||||
|
1. Миграция БД: `ALTER TABLE posts ADD COLUMN comment TEXT`.
|
||||||
|
2. Роуты: GET /candidates (двухпанельный), POST /candidates/bulk,
|
||||||
|
POST /posts/{id}/comment, POST /posts/{id}/reclassify, POST /posts/{id}/rewrite.
|
||||||
|
3. Шаблоны: candidates.html переписать (двухколоночная), добавить new/reclassify/
|
||||||
|
comment/rewrite формы.
|
||||||
|
4. Bulk-логика + группировка на сервере (Python).
|
||||||
|
5. Тест: ручной сценарий (создать 2-3 поста, сгруппировать, bulk-отклонить,
|
||||||
|
коммент, реклассификация).
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
Пользователю неудобно работать с карточками кандидатов в текущем виде. Сейчас:
|
||||||
|
- Страница кандидатов — это лента карточек (каждая с апрув-кнопкой), нет списка слева,
|
||||||
|
нет меню действий справа, нет группировки, нет bulk-операций.
|
||||||
|
- Пользователь описал целевой интерфейс: «схожий с почтовыми клиентами» — общий список
|
||||||
|
сообщений в левой панели, конкретное сообщение справа.
|
||||||
|
- Отклонять/одобрять по группам (например, «все старые посты отклонить») сейчас нельзя:
|
||||||
|
только по одному.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
Страница кандидатов `candidates` перерабатывается в двухпанельный интерфейс:
|
||||||
|
|
||||||
|
- **Левая панель** — список кандидатов (сообщений):
|
||||||
|
- элемент списка: аватар/иконка источника, заголовок (направление + краткий текст),
|
||||||
|
мета (источник, дата поступления, views/reactions, статус).
|
||||||
|
- активный/выбранный элемент подсвечивается.
|
||||||
|
- **группировка** (сворачиваемые группы): по источнику, по дате поступления,
|
||||||
|
по статусу (новые / одобренные / отклонённые).
|
||||||
|
- выбор нескольких элементов (чекбоксы) + **bulk-действия** над группой:
|
||||||
|
«отклонить все выбранные», «одобрить все выбранные».
|
||||||
|
- **Правая панель** — детальный просмотр выбранного кандидата:
|
||||||
|
- меню действий: **Опубликовать** (approve), **Отклонить** (reject), **Переклассифицировать**,
|
||||||
|
**Переписать новость под себя** (LLM-пересказ, ввод текста).
|
||||||
|
- блок «Мой комментарий» (textarea, сохраняется).
|
||||||
|
- оригинальный пост со всеми медиа (текст, картинки/видео, ссылки, мета источника).
|
||||||
|
- Хранение комментария — новое поле/таблица (comment в posts или отдельная
|
||||||
|
post_comments), связь с пользователем.
|
||||||
|
- Статусы: у поста уже есть status (new/rejected/published?) — использовать его для
|
||||||
|
группировки; добавить, если нет.
|
||||||
|
|
||||||
|
## Why Not
|
||||||
|
|
||||||
|
- Не делаем drag&drop, не делаем виртуальную бесконечную прокрутку — достаточно
|
||||||
|
пейджинга/группировки (минимум JS, стабильно).
|
||||||
|
- Не меняем существующие роуты approve/reject — только дополняем новыми (bulk,
|
||||||
|
переклассификация, коммент).
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- Комментарий: отдельная таблица или поле в posts? (предлагаю поле posts.comment TEXT)
|
||||||
|
- Переписывание: LLM (qwen) генерирует черновик, пользователь редактирует и публикует?
|
||||||
|
- «Переклассифицировать» — повторный запуск классификатора на посте?
|
||||||
|
- Группировка: одновременно несколько группировок или одна выбранная?
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# Spec — candidates-mail-ui
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Двухпанельный интерфейс кандидатов
|
||||||
|
|
||||||
|
Страница кандидатов разделена на две панели: слева список кандидатов (с группировкой),
|
||||||
|
справа — детальный просмотр выбранного кандидата с меню действий.
|
||||||
|
|
||||||
|
#### Scenario: Пользователь открывает список кандидатов
|
||||||
|
|
||||||
|
Given на странице `GET /candidates`
|
||||||
|
When страница загружается
|
||||||
|
Then левая панель показывает список кандидатов, правая — детали первого/последнего выбранного
|
||||||
|
And активный кандидат подсвечивается
|
||||||
|
|
||||||
|
#### Scenario: Группировка списка по источнику
|
||||||
|
|
||||||
|
Given список кандидатов
|
||||||
|
When пользователь выбирает группировку «по источнику»
|
||||||
|
Then кандидаты сгруппированы по источнику (канал/сайт)
|
||||||
|
And каждая группа сворачивается/разворачивается (без JS, через `<details>`)
|
||||||
|
|
||||||
|
#### Scenario: Группировка по дате поступления
|
||||||
|
|
||||||
|
When пользователь выбирает группировку «по дате поступления»
|
||||||
|
Then кандидаты сгруппированы по дате поступления (сегодня, вчера, ранее)
|
||||||
|
And порядок: новые сверху, старые ниже
|
||||||
|
|
||||||
|
### Requirement: Bulk-операции над выбранными кандидатами
|
||||||
|
|
||||||
|
Пользователь может выбрать несколько кандидатов (чекбоксы) и выполнить массовое
|
||||||
|
действие: одобрить все выбранные / отклонить все выбранные.
|
||||||
|
|
||||||
|
#### Scenario: Массовое отклонение выбранных
|
||||||
|
|
||||||
|
Given на левой панели несколько кандидатов с отмеченными чекбоксами
|
||||||
|
When пользователь нажимает «Отклонить выбранные»
|
||||||
|
Then все отмеченные кандидаты получают статус «отклонён»
|
||||||
|
And действие выполняется транзакционно (все или ни одного)
|
||||||
|
|
||||||
|
#### Scenario: Массовое одобрение выбранных
|
||||||
|
|
||||||
|
When пользователь нажимает «Одобрить выбранные»
|
||||||
|
Then каждый отмеченный кандидат публикуется (вызывается approve-логика)
|
||||||
|
|
||||||
|
#### Scenario: Отклонение всех старых постов группы
|
||||||
|
|
||||||
|
Given группа «ранее» (старые посты)
|
||||||
|
When пользователь выбирает «отклонить все в группе»
|
||||||
|
Then все посты группы получают статус «отклонён»
|
||||||
|
|
||||||
|
### Requirement: Действия над конкретным кандидатом
|
||||||
|
|
||||||
|
Правая панель содержит меню действий: Опубликовать, Отклонить, Переклассифицировать,
|
||||||
|
Переписать новость под себя, блок «Мой комментарий», оригинальный пост со всеми медиа.
|
||||||
|
|
||||||
|
#### Scenario: Опубликовать кандидата
|
||||||
|
|
||||||
|
Given выбран кандидат в правой панели
|
||||||
|
When пользователь нажимает «Опубликовать»
|
||||||
|
Then пост публикуется (существующая логика approve, publisher)
|
||||||
|
|
||||||
|
#### Scenario: Отклонить кандидата
|
||||||
|
|
||||||
|
When пользователь нажимает «Отклонить»
|
||||||
|
Then пост получает статус «отклонён» и исчезает из активных кандидатов
|
||||||
|
|
||||||
|
#### Scenario: Переклассифицировать кандидата
|
||||||
|
|
||||||
|
When пользователь нажимает «Переклассифицировать»
|
||||||
|
Then запускается классификатор для данного поста
|
||||||
|
And направление/relevance/interest обновляются, изменения видны на карточке
|
||||||
|
|
||||||
|
#### Scenario: Переписать новость под себя
|
||||||
|
|
||||||
|
When пользователь нажимает «Переписать новость»
|
||||||
|
Then LLM генерирует черновик пересказа от первого лица (без копипасты)
|
||||||
|
And черновик отображается в текстовом поле для редактирования
|
||||||
|
And пользователь может отредактировать и затем опубликовать
|
||||||
|
|
||||||
|
#### Scenario: Комментарий к кандидату
|
||||||
|
|
||||||
|
Given в правой панели блок «Мой комментарий»
|
||||||
|
When пользователь вводит текст и сохраняет
|
||||||
|
Then комментарий сохраняется и отображается при повторном открытии кандидата
|
||||||
|
|
||||||
|
#### Scenario: Оригинальный пост со всеми медиа
|
||||||
|
|
||||||
|
Given выбран кандидат с медиа
|
||||||
|
Then правая панель показывает оригинальный текст поста, все изображения/видео
|
||||||
|
(через роут /media/), мета источника (канал, время, views/reactions)
|
||||||
|
|
||||||
|
### Requirement: Фильтрация и поиск по списку
|
||||||
|
|
||||||
|
Список кандидатов можно фильтровать по статусу (новые/одобренные/отклонённые)
|
||||||
|
и искать по тексту.
|
||||||
|
|
||||||
|
#### Scenario: Фильтр по статусу
|
||||||
|
|
||||||
|
Given на странице кандидатов
|
||||||
|
When пользователь выбирает фильтр «новые»
|
||||||
|
Then в списке остаются только новые кандидаты
|
||||||
|
|
||||||
|
#### Scenario: Поиск по тексту
|
||||||
|
|
||||||
|
When пользователь вводит текст в поиск
|
||||||
|
Then список фильтруется по вхождению в текст поста
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# tasks.md — candidates-mail-ui
|
||||||
|
|
||||||
|
## Proposal
|
||||||
|
- [x] proposal.md — двухпанельный интерфейс кандидатов (почтовый клиент), группировка, bulk
|
||||||
|
- [x] design.md — архитектура (роуты, БД, шаблоны, минимум JS)
|
||||||
|
|
||||||
|
## Implement
|
||||||
|
- [ ] Проверить схему posts (status, поля) — уточнить, какие статусы есть
|
||||||
|
- [ ] Миграция: `ALTER TABLE posts ADD COLUMN comment TEXT` (или таблица comments)
|
||||||
|
- [ ] GET /candidates — двухпанельный рендер (левая колонка список+группировка, правая — детально)
|
||||||
|
- [ ] Левая панель: группировка по source/date/status (сворачиваемые <details>), чекбоксы
|
||||||
|
- [ ] POST /candidates/bulk — approve/reject по ids[] (транзакция)
|
||||||
|
- [ ] POST /posts/{id}/comment — сохранить комментарий (comment TEXT)
|
||||||
|
- [ ] POST /posts/{id}/reclassify — перезапуск классификатора на посте
|
||||||
|
- [ ] POST /posts/{id}/rewrite — LLM-пересказ (qwen), возврат черновика для редактирования
|
||||||
|
- [ ] Правая панель: меню действий, блок комментария, оригинальный пост со всеми медиа
|
||||||
|
- [ ] Шаблоны: candidates.html (двухколоночная), формы новых действий
|
||||||
|
- [ ] Тест: создание 2-3 тестовых постов, группировка, bulk-отклонение, коммент, реклассификация
|
||||||
|
- [ ] `openspec validate candidates-mail-ui` — чисто
|
||||||
|
- [ ] Обновить STATUS.md / WALKTHROUGH.md
|
||||||
Reference in New Issue
Block a user