5.1 KiB
Design
1. БД: колонка is_read
db/schema.sql — в CREATE TABLE posts добавить:
is_read INTEGER DEFAULT 0, -- 1 = пост просмотрен (открыт в правой панели)
Миграция существующей БД — при старте веб-приложения (идемпотентно):
в начале web/app.py (после DB_PATH) или в _db():
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 задан и пост найден — пометить
прочитанным ДО рендера (чтобы карточка сразу стала обычной):
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):
<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>
Стало:
<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>:
.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. - Ручная проверка: список по умолчанию по дате; непрочитанные жирные; открытый пост — голубая заливка; после открытия карточка перестаёт быть жирной.