mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 09:55:03 +00:00
117 lines
5.1 KiB
Markdown
117 lines
5.1 KiB
Markdown
## 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`.
|
||
- Ручная проверка: список по умолчанию по дате; непрочитанные жирные;
|
||
открытый пост — голубая заливка; после открытия карточка перестаёт быть
|
||
жирной. |