From c09dff08acc54c5dca7ab6ae73611a2cffe5cf1e Mon Sep 17 00:00:00 2001 From: kpa39l Date: Tue, 15 Sep 2026 16:42:55 +0000 Subject: [PATCH] =?UTF-8?q?feat(candidates):=20=D1=84=D0=BB=D0=B0=D0=B3=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=87=D0=B8=D1=82=D0=B0=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20(is=5Fread),=20=D0=B6=D0=B8=D1=80=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=BD=D0=B5=D0=BF=D1=80=D0=BE=D1=87=D0=B8=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5,=20=D0=B3=D0=BE=D0=BB=D1=83?= =?UTF-8?q?=D0=B1=D0=B0=D1=8F=20=D0=B7=D0=B0=D0=BB=D0=B8=D0=B2=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=B2=D1=8B=D0=B1=D1=80=D0=B0=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?,=20=D0=B4=D0=B5=D1=84=D0=BE=D0=BB=D1=82=D0=BD=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D0=B8=D1=80=D0=BE=D0=B2=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=20=D0=B4=D0=B0=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- STATUS.md | 1 + .../candidates-read-flag/.openspec.yaml | 3 + .../changes/candidates-read-flag/design.md | 117 ++++++++++++++++++ .../changes/candidates-read-flag/proposal.md | 45 +++++++ .../changes/candidates-read-flag/tasks.md | 15 +++ web/app.py | 26 +++- web/templates/base.html | 5 + web/templates/candidates.html | 4 +- 8 files changed, 211 insertions(+), 5 deletions(-) create mode 100644 openspec/changes/candidates-read-flag/.openspec.yaml create mode 100644 openspec/changes/candidates-read-flag/design.md create mode 100644 openspec/changes/candidates-read-flag/proposal.md create mode 100644 openspec/changes/candidates-read-flag/tasks.md diff --git a/STATUS.md b/STATUS.md index 6212d5b..2275c36 100644 --- a/STATUS.md +++ b/STATUS.md @@ -159,6 +159,7 @@ CLASSIFY_TIMEOUT=20 .venv/bin/python -m classifier.classify --db db/vesti.db --l - Тихие OpenSpec-change: `candidates-only-external-new`, `keep-candidates-context` — задачи [x], валидны; архивировать после подтверждения. - Правый блок действий: кнопки «🤖 Обработать моделью / ✍️ Переписать / 🚫 Отклонить» вынесены в ОДИН ряд НАД полем комментария, «✅ Опубликовать» — в начале approve-формы, поле комментария + «💾 Сохранить комментарий» — ниже (change `candidates-buttons-order`). - После «🚫 Отклонить» (и bulk reject/reject-old) — остаёмся на странице кандидатов в текущей группировке, НЕ уходим в «🗑 Отклонённые» (change `reject-stay-candidates`); отклонённые — через фильтр статуса. +- Список кандидатов: флаг «прочитан» — непрочитанные карточки жирные, выбранный пост — светло-голубая заливка (класс active-row), открытие поста помечает is_read=1; группировка по умолчанию — «дата» (хронология получения) (change `candidates-read-flag`). ## Ключевые артефакты - /opt/vesti/AGENT.MD — правила проекта (обязательно: все изменения через OpenSpec) diff --git a/openspec/changes/candidates-read-flag/.openspec.yaml b/openspec/changes/candidates-read-flag/.openspec.yaml new file mode 100644 index 0000000..4258753 --- /dev/null +++ b/openspec/changes/candidates-read-flag/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +skip_specs: true +created: 2026-09-15 diff --git a/openspec/changes/candidates-read-flag/design.md b/openspec/changes/candidates-read-flag/design.md new file mode 100644 index 0000000..a664a81 --- /dev/null +++ b/openspec/changes/candidates-read-flag/design.md @@ -0,0 +1,117 @@ +## Design + +### 1. БД: колонка is_read + +`db/schema.sql` — в CREATE TABLE posts добавить: + +```sql +is_read INTEGER DEFAULT 0, -- 1 = пост просмотрен (открыт в правой панели) +``` + +Миграция существующей БД — при старте веб-приложения (идемпотентно): +в начале `web/app.py` (после `DB_PATH`) или в `_db()`: + +```python +def _migrate(): + """Идемпотентные миграции при старте (веб-сервис).""" + conn = sqlite3.connect(DB_PATH, timeout=10) + cols = [r[1] for r in conn.execute("PRAGMA table_info(posts)").fetchall()] + if "is_read" not in cols: + conn.execute("ALTER TABLE posts ADD COLUMN is_read INTEGER DEFAULT 0") + conn.commit() + conn.close() +``` + +Вызвать один раз при импорте (перед `app = FastAPI(...)` или после). + +### 2. Дефолтная группировка — дата + +- `GET /candidates`: параметр `group_by: str = "date"` (было `"source"`). +- `POST /candidates/select`: `group_by: str = Form("date")` (было `"source"`). +- `POST /candidates/bulk`: `group_by: str = Form("date")` — тоже, чтобы + формы левой панели (bulk) сохраняли выбранную группировку по умолчанию. + +### 3. Прочтение при выборе поста + +В `GET /candidates`: когда `selected` задан и пост найден — пометить +прочитанным ДО рендера (чтобы карточка сразу стала обычной): + +```python +if selected: + conn.execute("UPDATE posts SET is_read=1 WHERE id=? AND is_read=0", (selected,)) + conn.commit() +``` + +(селект уже найден выше; UPDATE до `conn.close()`). + +В `POST /candidates/select` — после редиректа на +`/candidates?selected=id&group_by=...` это покроет выбор; но select — +обычная форма, редирект идёт на GET, который и помечает. Отдельный UPDATE +в select не нужен (GET сделает). + +### 4. Шаблон: карточки списка + +`web/templates/candidates.html`, блок `{% for p in g.posts %}`: + +Сейчас (строки 78–92): + +```html +
+ + {{ (p.text or '')[:90] }} + ... + + +
+``` + +Стало: + +```html +
+ + {{ (p.text or '')[:90] }} + ... + + +
+``` + +- `is_read=0` → `fw-bold` на всей карточке (текст жирный); +- `is_read=1` → обычный; +- выбранный → класс `active-row` → светло-голубая заливка. + +Класс `active-row` — в base.html или candidates.html ` @@ -19,6 +22,8 @@ Кандидаты Опубликовано Метрики + Краулеры + Источники Выйти diff --git a/web/templates/candidates.html b/web/templates/candidates.html index 5c099bb..a16a02e 100644 --- a/web/templates/candidates.html +++ b/web/templates/candidates.html @@ -75,8 +75,8 @@
{% for p in g.posts %} -