## 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
``` Стало: ```html ``` - `is_read=0` → `fw-bold` на всей карточке (текст жирный); - `is_read=1` → обычный; - выбранный → класс `active-row` → светло-голубая заливка. Класс `active-row` — в base.html или candidates.html `