Files
vesti/openspec/changes/archive/2026-09-16-candidates-read-flag/design.md
T

117 lines
5.1 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.
## 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`.
- Ручная проверка: список по умолчанию по дате; непрочитанные жирные;
открытый пост — голубая заливка; после открытия карточка перестаёт быть
жирной.