openspec: архив 14 завершённых change-ов (веб-фиксы, crawler-queue, own-content-hub, publisher-service); спеки влиты в openspec/specs

This commit is contained in:
kpa39l
2026-09-16 17:01:00 +00:00
parent 584582a48c
commit 771f6a8276
88 changed files with 1632 additions and 3 deletions
@@ -0,0 +1,3 @@
schema: spec-driven
skip_specs: true
created: 2026-09-15
@@ -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
<div class="list-group-item d-flex align-items-start">
<a class="flex-grow-1 text-decoration-none{% if selected and selected.id == p.id %} fw-bold text-primary{% endif %}"
href="...">
<span class="d-block text-truncate">{{ (p.text or '')[:90] }}</span>
...
</a>
<input type="checkbox" ...>
</div>
```
Стало:
```html
<div class="list-group-item d-flex align-items-start{% if selected and selected.id == p.id %} active-row{% endif %}{% if not p.is_read %} fw-bold{% endif %}">
<a class="flex-grow-1 text-decoration-none"
href="...">
<span class="d-block text-truncate">{{ (p.text or '')[:90] }}</span>
...
</a>
<input type="checkbox" ...>
</div>
```
- `is_read=0` → `fw-bold` на всей карточке (текст жирный);
- `is_read=1` → обычный;
- выбранный → класс `active-row` → светло-голубая заливка.
Класс `active-row` — в base.html или candidates.html `<style>`:
```css
.list-group-item.active-row {
background-color: #e3f2fd; /* светло-голубой */
/* не перебиваем hover/оригинал: border, text — наследуются */
}
```
(используем кастомный класс, т.к. bootstrap `active` меняет цвет текста на
белый — не нужно; `bg-info`/`bg-light` — слишком сильные/неоднородные.)
### 5. Жирный шрифт только для непрочитанного
`fw-bold` на контейнере карточки делает жирным и текст, и метаданные —
это нормально для почтового клиента (непрочитанное письмо — весь блок
жирный). Ссылка остаётся `text-decoration-none`, цвет «primary» у
выбранного убираем (заливка вместо цвета).
## Верификация
- `openspec validate candidates-read-flag` — чисто.
- Миграция: запуск приложения (TestClient/import) создаёт колонку is_read;
существующие посты получают 0.
- Тест: GET /candidates?selected=N → в HTML карточка N имеет `active-row`
и НЕ `fw-bold` (после прочтения); у непрочитанного поста — `fw-bold`.
- Дефолт: GET /candidates (без group_by) → в шаблоне `group_by == 'date'`,
группы подписаны «Сегодня/Вчера/…».
- Перезапуск `sudo systemctl restart vesti-web`.
- Ручная проверка: список по умолчанию по дате; непрочитанные жирные;
открытый пост — голубая заливка; после открытия карточка перестаёт быть
жирной.
@@ -0,0 +1,45 @@
## Why
Список кандидатов — «почтовый клиент»: слева список карточек, справа детали.
Сейчас все карточки выглядят одинаково, не видно, какие посты уже смотрели,
а какой пост выбран (открыт) — не выделен, кроме цвета текста ссылки.
Пользователь хочет как в почтовых клиентах:
- непрочитанные посты — жирным шрифтом;
- прочитанные — обычным (как сейчас);
- выбранный (открытый) пост — светло-голубая заливка всей карточки.
Плюс: по умолчанию группировка должна быть ПО ДАТЕ (хронология получения),
а не по источнику.
## What Changes
- БД: колонка `posts.is_read INTEGER DEFAULT 0` (0 = не прочитан, 1 = прочитан).
- Web:
- выбор поста (GET /candidates?selected=N или POST /candidates/select)
помечает его `is_read=1`;
- карточка непрочитанного поста в списке — жирный текст (`fw-bold` на
заголовке/тексте), прочитанного — обычный (как сейчас);
- карточка выбранного поста — светло-голубая заливка всей карточки
(вместо текущего `fw-bold text-primary` на ссылке);
- дефолт `group_by` в GET /candidates и POST /candidates/select — `date`
вместо `source` (хронологический порядок получения: сегодня → вчера →
неделя → раньше).
## Why Not
- Не менять статус при публикации/отклонении: прочитанность — отдельный
флаг просмотра, не связан со статусом.
- Не делать авто-прочтение всех при заходе на страницу: только выбранный
пост становится прочитанным (как в почте).
- Группировка по дате — дефолт, но переключатель (источник/статус) остаётся.
## Impact
- Файлы: `db/schema.sql` (колонка), `web/app.py` (миграция при старте +
дефолт группировки + прочтение при выборе), `web/templates/candidates.html`
(классы карточек).
- Данные: ALTER TABLE (новые посты is_read=0 — непрочитанные; старые
после миграции тоже 0).
- Сервис: `vesti-web` (:8400) — перезапуск.
- Rollback: убрать колонку/правки и перезапустить.
@@ -0,0 +1,15 @@
# candidates-read-flag
- [x] Создан OpenSpec change (proposal/design) — `skip_specs: true`
- [x] `openspec validate candidates-read-flag` — чисто
- [x] db/schema.sql: колонка posts.is_read INTEGER DEFAULT 0
- [x] web/app.py: миграция is_read при старте (_migrate, идемпотентно)
- [x] web/app.py: GET /candidates и POST select/bulk: group_by по умолчанию 'date'
- [x] web/app.py: при selected → UPDATE posts SET is_read=1
- [x] candidates.html: непрочитанные — fw-bold, выбранный — active-row (светло-голубая заливка)
- [x] base.html: CSS .list-group-item.active-row (#dcecfc)
- [x] Тест (TestClient): миграция, прочтение при selected, дефолт date, классы карточек
- [x] Перезапуск `sudo systemctl restart vesti-web`
- [x] Ручная проверка в браузере: дата по умолчанию, жирные непрочитанные, голубая заливка выбранного
- [x] Обновить STATUS.md / TODO.md
- [x] Бэкап (крон 2:45 делает сам, вручную НЕ запускать)