Files

5.1 KiB
Raw Permalink Blame History

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