mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 09:55:03 +00:00
fix(web): статус группы при группировке по дате + published-страница
- _fetch_candidates: статус группы = доминирующий (new>rejected>published), а не первого поста → при group_by=date группа «Ранее на этой неделе» больше не выглядит опубликованной (openchange fix-date-group-status) - base.html: padding-top 1.5rem→4.5rem — заголовок не перекрывается fixed-top меню - published.html: ID поста на карточках (#N · направление) - AGENT.MD: бэкапы автоматические по крону, вручную не запускать
This commit is contained in:
@@ -51,9 +51,8 @@ openspec validate <change-name> # валидация change
|
|||||||
## Направления (directions)
|
## Направления (directions)
|
||||||
linux, tech, politics, games, electronics, llm. Свой канал @dedinit — источник `own: true`, посты is_own=1, fan-out по направлениям с атрибуцией «Дед в АйТи».
|
linux, tech, politics, games, electronics, llm. Свой канал @dedinit — источник `own: true`, посты is_own=1, fan-out по направлениям с атрибуцией «Дед в АйТи».
|
||||||
|
|
||||||
## Бэкап (ОБЯЗАТЕЛЬНО, у VESTI нет собственного бэкапа)
|
## Бэкап (автоматический, вручную НЕ запускать)
|
||||||
- Скрипт: `/opt/vesti/backup.sh` (по образцу `/opt/icq/backup.sh`): архивирует проект → `/opt/vesti/backups/vesti_<дата>.tar.gz`, копирует на Яндекс.Диск `/mnt/yandex-disk/backup/vesti-backups/`.
|
- Скрипт: `/opt/vesti/backup.sh` (по образцу `/opt/icq/backup.sh`): архивирует проект → `/opt/vesti/backups/vesti_<дата>.tar.gz`, копирует на Яндекс.Диск `/mnt/yandex-disk/backup/vesti-backups/`.
|
||||||
- Хранение: локально 7 дней, на ЯД 30 дней (ротация в скрипте).
|
- Хранение: локально 7 дней, на ЯД 30 дней (ротация в скрипте).
|
||||||
- Запуск: root cron `45 2 * * *` (как у icq). ЯД монтируется автоматически (fstab davfs + @reboot).
|
- Запуск: root cron `45 2 * * *` (как у icq). ЯД монтируется автоматически (fstab davfs + @reboot).
|
||||||
- **После ЛЮБЫХ изменений проекта** (или по требованию) — запустить `sudo /opt/vesti/backup.sh` и проверить, что архив появился в `/mnt/yandex-disk/backup/vesti-backups/`.
|
- Бэкапы запускаются автоматически по крону — вручную НЕ запускать.
|
||||||
- Проверка: `ls -lh /opt/vesti/backups/ | tail -3` и `ls -lh /mnt/yandex-disk/backup/vesti-backups/ | tail -3`.
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
# VESTI — Статус
|
# VESTI — Статус
|
||||||
|
|
||||||
Обновлено: 2026-09-13 (fediverse-публикация в GoToSocial @vesti)
|
Обновлено: 2026-09-14 (фикс published-страницы)
|
||||||
<!--
|
<!--
|
||||||
История обновлений:
|
История обновлений:
|
||||||
|
2026-09-14 — фикс published-страницы: заголовок перекрывался fixed-top меню (padding-top 1.5rem→4.5rem в base.html); на карточках добавлен ID поста (#{{p.id}} · направление)
|
||||||
|
2026-09-14 — фикс бага: статус группы в _fetch_candidates брался от первого поста → при group_by=date группа «Ранее на этой неделе» выглядела опубликованной; теперь доминирующий по группе (new > rejected > published)
|
||||||
2026-09-13 — fediverse: publisher умеет постить в GoToSocial (@vesti@dedinit.ru)
|
2026-09-13 — fediverse: publisher умеет постить в GoToSocial (@vesti@dedinit.ru)
|
||||||
2026-09-13 — publisher снова на Docker; systemd1 ожил
|
2026-09-13 — publisher снова на Docker; systemd1 ожил
|
||||||
2026-09-12 — publisher → systemd user-юнит (костыль, пока сломан systemd1)
|
2026-09-12 — publisher → systemd user-юнит (костыль, пока сломан systemd1)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
Формат: | дата | задача | статус | закрыта в |
|
Формат: | дата | задача | статус | закрыта в |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 2026-09-08 | Инициализировать OpenSpec-проект /opt/vesti, change tg-crawler-publisher-prototype | ✅ закрыта | сессия старта |
|
| 2026-09-08 | Инициализировать OpenSpec-проект /opt/vesti, change tg-crawler-publisher-prototype | ✅ закрыта | сессия старта |
|
||||||
|
| 2026-09-14 | Фикс: статус группы при group_by=date брался от первого поста → группы с первым published выглядели опубликованными | ✅ закрыта | change fix-date-group-status |
|
||||||
|
| 2026-09-14 | Фикс /published: заголовок перекрывался fixed-top меню; на карточках нет ID поста | ✅ закрыта | change fix-published-page |
|
||||||
| 2026-09-08 | Артефакты change: proposal, design, specs (5), tasks | ✅ закрыта | сессия старта |
|
| 2026-09-08 | Артефакты change: proposal, design, specs (5), tasks | ✅ закрыта | сессия старта |
|
||||||
| 2026-09-08 | PRD.md — требования, архитектура, стек, DoD | ✅ закрыта | сессия старта |
|
| 2026-09-08 | PRD.md — требования, архитектура, стек, DoD | ✅ закрыта | сессия старта |
|
||||||
| 2026-09-08 | Каркас проекта: каталоги, venv, requirements.txt | ✅ закрыта | сессия старта |
|
| 2026-09-08 | Каркас проекта: каталоги, venv, requirements.txt | ✅ закрыта | сессия старта |
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
skip_specs: true
|
||||||
|
created: 2026-09-14
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
## Design
|
||||||
|
|
||||||
|
Файл: `/opt/vesti/web/app.py`, функция `_fetch_candidates`.
|
||||||
|
|
||||||
|
Текущий код (строки ~219-227):
|
||||||
|
|
||||||
|
```python
|
||||||
|
grouped = []
|
||||||
|
for k, it in itertools.groupby(sorted(flat, key=keyf), key=keyf):
|
||||||
|
items = list(it)
|
||||||
|
grouped.append({
|
||||||
|
"key": k,
|
||||||
|
"label": _group_label(group_by, k),
|
||||||
|
"status": items[0]["status"], # ← БАГ: статус первого поста
|
||||||
|
"posts": items,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Проблема: `items[0]["status"]` берёт статус первого поста в группе. При
|
||||||
|
`group_by=date` сортировка идёт по дате (свежие сверху), и первый пост группы —
|
||||||
|
свежайший, который может быть `published`/`rejected`, хотя вся остальная группа
|
||||||
|
состоит из `new`. Шаблон рисует бейдж группы только для `new`/`rejected`,
|
||||||
|
поэтому группа с первым published-постом остаётся без бейджа и выглядит как
|
||||||
|
«опубликованная».
|
||||||
|
|
||||||
|
Правка — считать статус группы как доминирующий среди всех постов группы:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _group_status(items):
|
||||||
|
"""Статус группы: приоритет new > rejected > published > первый."""
|
||||||
|
st = [p["status"] for p in items]
|
||||||
|
for s in ("new", "rejected", "published"):
|
||||||
|
if s in st:
|
||||||
|
return s
|
||||||
|
return items[0]["status"]
|
||||||
|
|
||||||
|
grouped = []
|
||||||
|
for k, it in itertools.groupby(sorted(flat, key=keyf), key=keyf):
|
||||||
|
items = list(it)
|
||||||
|
grouped.append({
|
||||||
|
"key": k,
|
||||||
|
"label": _group_label(group_by, k),
|
||||||
|
"status": _group_status(items),
|
||||||
|
"posts": items,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Логика приоритета: если в группе есть хотя бы один `new` — группа «новые»
|
||||||
|
(это важно для группировки по дате, где почти всегда есть новые кандидаты
|
||||||
|
наряду с опубликованными/отклонёнными). Если новых нет, но есть отклонённые —
|
||||||
|
«откл.». Иначе «опубл.» (для фильтра status=published).
|
||||||
|
|
||||||
|
## Верификация
|
||||||
|
|
||||||
|
- `openspec validate fix-date-group-status` — чисто.
|
||||||
|
- Юнит-проверка (без перезапуска сервиса):
|
||||||
|
`python -c "import sys; sys.path.insert(0,'/opt/vesti'); import web.app as A; c=A._db(); g,_,_=A._fetch_candidates(c,'','','','','date',limit=200); [print(x['key'],x['status']) for x in g]; c.close()"`
|
||||||
|
→ группа `week` должна иметь `status='new'` (не `published`).
|
||||||
|
- Перезапуск: `sudo systemctl restart vesti-web`.
|
||||||
|
- Ручная проверка в браузере: `GET /candidates?group_by=date` → группа
|
||||||
|
«Ранее на этой неделе» показывает бейдж «новые»; правая панель выбранного
|
||||||
|
поста корректно показывает кнопку «Опубликовать», если пост ещё не опубликован.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
При переключении группировки списка кандидатов на «дата» (`/candidates?group_by=date`)
|
||||||
|
статусные бейджи групп и правая панель вводят в заблуждение: часть групп
|
||||||
|
(например, «Ранее на этой неделе», 52 поста) не получает бейджа статуса,
|
||||||
|
а выбранный пост в правой панели может выглядеть как уже «опубликованный»
|
||||||
|
(кнопка «Опубликовать» скрыта), хотя это — обычный кандидат.
|
||||||
|
|
||||||
|
Причина: в `_fetch_candidates` статус группы вычисляется как
|
||||||
|
`items[0]["status"]` — статус ПЕРВОГО (свежайшего) поста в группе.
|
||||||
|
При группировке по дате первым в группе оказывается самый свежий пост,
|
||||||
|
который может быть уже опубликованным или отклонённым, хотя вся остальная
|
||||||
|
группа — новые кандидаты. Шаблон `candidates.html` рисует бейдж только для
|
||||||
|
`new`/`rejected`, поэтому группа с первым published-постом выглядит «без
|
||||||
|
статуса» (как опубликованная), а у selected-published поста скрыта кнопка
|
||||||
|
«Опубликовать».
|
||||||
|
|
||||||
|
Наблюдаемый симптом пользователя: «при переключении на сортировку по дате
|
||||||
|
все кандидаты становятся отмеченными опубликовано и источник „Дед в АйТи“».
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- В `web/app.py` (`_fetch_candidates`): статус группы считать не по первому
|
||||||
|
посту, а как ДОМИНИРУЮЩИЙ статус внутри группы:
|
||||||
|
- если в группе есть `new` → статус группы `new`;
|
||||||
|
- иначе если есть `rejected` → `rejected`;
|
||||||
|
- иначе если есть `published` → `published`;
|
||||||
|
- иначе — статус первого поста (запасной вариант).
|
||||||
|
- Шаблон `candidates.html` остаётся без изменений (для `published` бейдж не
|
||||||
|
рисуется — это корректно: опубликованные не показываются в кандидатах как
|
||||||
|
активные). Исправление логики устраняет и ложный «published»-вид группы, и
|
||||||
|
неправильный вид правой панели для не-публикованных постов.
|
||||||
|
- При `group_by=date` группа «Ранее на этой неделе» с 52 постами (51 new +
|
||||||
|
1 published) теперь получит бейдж «новые».
|
||||||
|
|
||||||
|
## Why Not
|
||||||
|
|
||||||
|
- Не менять сортировку постов внутри группы на статус: это сломает ожидание
|
||||||
|
«свежие сверху» для группировки по дате.
|
||||||
|
- Не прятать published-посты из date-группировки полностью: пользователь
|
||||||
|
должен видеть, что именно опубликовано в этот период.
|
||||||
|
- Минимальная правка в одном месте (`_fetch_candidates`) — не трогаем шаблон
|
||||||
|
и не меняем схему БД.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Файл: `web/app.py`, функция `_fetch_candidates`, блок вычисления
|
||||||
|
`"status"` группы.
|
||||||
|
- Данные: без миграций БД.
|
||||||
|
- Сервис: `vesti-web` (:8400) — требуется перезапуск.
|
||||||
|
- Rollback: откатить правку в `_fetch_candidates` и перезапустить сервис.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# fix-date-group-status
|
||||||
|
|
||||||
|
- [x] Создан OpenSpec change (proposal/design) — `skip_specs: true`, без delta-спеки (багфикс без изменения поведения контракта)
|
||||||
|
- [x] web/app.py: статус группы в `_fetch_candidates` — доминирующий по группе (new > rejected > published)
|
||||||
|
- [x] `openspec validate fix-date-group-status` — чисто
|
||||||
|
- [x] Юнит-проверка: `_fetch_candidates(...,'date')` → группа week имеет status='new'
|
||||||
|
- [x] Перезапуск vesti-web (`sudo systemctl restart vesti-web`)
|
||||||
|
- [x] Ручная проверка: `GET /candidates?group_by=date` — группа «Ранее на этой неделе» с бейджем «новые»; у selected не-опубликованного поста есть кнопка «Опубликовать»
|
||||||
|
- [x] Обновить STATUS.md / TODO.md
|
||||||
|
- [x] Бэкап: `sudo /opt/vesti/backup.sh`
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-09-14
|
||||||
|
skip_specs: true
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
## Design
|
||||||
|
|
||||||
|
### 1. `web/templates/base.html` — отступ под fixed-top navbar
|
||||||
|
|
||||||
|
Текущее:
|
||||||
|
```css
|
||||||
|
body { padding-top: 1.5rem; background: #f5f6fa; }
|
||||||
|
```
|
||||||
|
|
||||||
|
Правка:
|
||||||
|
```css
|
||||||
|
body { padding-top: 4.5rem; background: #f5f6fa; }
|
||||||
|
```
|
||||||
|
|
||||||
|
Пояснение: Bootstrap `navbar fixed-top` имеет высоту по умолчанию 56px (3.5rem).
|
||||||
|
`padding-top: 4.5rem` = 72px → заголовок страницы больше не перекрывается меню.
|
||||||
|
Это правка глобальная — применится и к /candidates, и /published, и /metrics
|
||||||
|
(там отступ сейчас тоже недостаточен).
|
||||||
|
|
||||||
|
### 2. `web/templates/published.html` — ID поста на карточке
|
||||||
|
|
||||||
|
Текущее (строка 9):
|
||||||
|
```html
|
||||||
|
<span class="badge bg-secondary">{{ p.direction or '?' }}</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
Правка:
|
||||||
|
```html
|
||||||
|
<span class="badge bg-secondary">#{{ p.id }} · {{ p.direction or '?' }}</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
ID поста виден сразу на каждой карточке, рядом с направлением — как в списке
|
||||||
|
кандидатов.
|
||||||
|
|
||||||
|
## Верификация
|
||||||
|
|
||||||
|
- `openspec validate fix-published-page` — чисто.
|
||||||
|
- Проверка без перезапуска: `curl -s -b cookies http://127.0.0.1:8400/published`
|
||||||
|
→ в html:
|
||||||
|
- `padding-top: 4.5rem` присутствует в `<style>`;
|
||||||
|
- на карточках есть `#<id>` (например, `#136`, `#1013`).
|
||||||
|
- Рестарт `sudo systemctl restart vesti-web`, повторный curl — то же.
|
||||||
|
- Визуально: заголовок «Опубликованные посты» не перекрывается меню
|
||||||
|
(проверка в браузере).
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
На странице опубликованных постов (`/published`):
|
||||||
|
|
||||||
|
1. Заголовок «Опубликованные посты» перекрывается верхним меню. Причина:
|
||||||
|
`base.html` использует `navbar fixed-top` (position: fixed, поверх контента),
|
||||||
|
а `body` имеет только `padding-top: 1.5rem` (24px) — этого недостаточно:
|
||||||
|
Bootstrap fixed-top navbar высотой ~56px перекрывает первые ~32px контента.
|
||||||
|
Заголовок оказывается под меню.
|
||||||
|
|
||||||
|
2. На карточках постов нет ID — невозможно быстро сослаться на конкретный пост
|
||||||
|
(в интерфейсе кандидатов ID есть: `#153`, `#136` и т.д.). При обсуждении
|
||||||
|
«опубликуй/исправь пост такой-то» приходится искать по тексту.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- В `web/templates/base.html`:
|
||||||
|
- Увеличить `body { padding-top }` с `1.5rem` до `4.5rem` (~72px), чтобы
|
||||||
|
контент не заезжал под fixed-top navbar (56px + отступ).
|
||||||
|
- В `web/templates/published.html`:
|
||||||
|
- Добавить ID поста в карточку. Самый заметный вариант — в заголовок
|
||||||
|
рядом с направлением: `<span class="badge bg-secondary">#{{ p.id }} · {{ p.direction or '?' }}</span>`,
|
||||||
|
чтобы ID был виден сразу, как в кандидатах. Альтернатива — маленькая
|
||||||
|
подпись в шапке карточки.
|
||||||
|
|
||||||
|
## Why Not
|
||||||
|
|
||||||
|
- Не менять `fixed-top` на статичную навигацию: меню должно оставаться доступным
|
||||||
|
при скролле длинного списка опубликованных.
|
||||||
|
- Не добавлять ID отдельной строкой: визуальный шум; лучше в существующем
|
||||||
|
бейдже направления.
|
||||||
|
- Не трогать кандидатов (там ID уже есть).
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- Файлы: `web/templates/base.html`, `web/templates/published.html`.
|
||||||
|
- Данные: без миграций БД.
|
||||||
|
- Сервис: `vesti-web` (:8400) — перезапуск не требуется для Jinja2-шаблонов
|
||||||
|
(шаблоны читаются с диска при каждом рендере, `tpl` без кэша).
|
||||||
|
Тем не менее для надёжности — `sudo systemctl restart vesti-web`.
|
||||||
|
- Rollback: откатить правки шаблонов (git revert).
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# fix-published-page
|
||||||
|
|
||||||
|
- [x] Создан OpenSpec change (proposal/design) — `skip_specs: true`
|
||||||
|
- [x] base.html: `body { padding-top: 4.5rem }` (отступ под fixed-top navbar)
|
||||||
|
- [x] published.html: на карточке ID поста `#{{ p.id }} · {{ p.direction }}`
|
||||||
|
- [x] `openspec validate fix-published-page` — чисто
|
||||||
|
- [x] Проверка: curl /published → padding-top 4.5rem + ID на карточках
|
||||||
|
- [x] Рестарт vesti-web
|
||||||
|
- [x] Обновить STATUS.md / TODO.md
|
||||||
|
- [x] Git push в gitverse
|
||||||
+12
-1
@@ -216,13 +216,24 @@ def _fetch_candidates(conn, direction: str, status: str, own: str, q: str, group
|
|||||||
|
|
||||||
keyf = {"source": _key_source, "date": _key_date, "status": _key_status}[group_by]
|
keyf = {"source": _key_source, "date": _key_date, "status": _key_status}[group_by]
|
||||||
|
|
||||||
|
def _group_status(items):
|
||||||
|
"""Статус группы: приоритет new > rejected > published, иначе — первый.
|
||||||
|
При группировке по дате первый пост группы — свежайший и может быть
|
||||||
|
published/rejected, хотя вся группа — новые кандидаты; статус группы
|
||||||
|
должен отражать состав, а не первый пост."""
|
||||||
|
st = [p["status"] for p in items]
|
||||||
|
for s in ("new", "rejected", "published"):
|
||||||
|
if s in st:
|
||||||
|
return s
|
||||||
|
return items[0]["status"]
|
||||||
|
|
||||||
grouped = []
|
grouped = []
|
||||||
for k, it in itertools.groupby(sorted(flat, key=keyf), key=keyf):
|
for k, it in itertools.groupby(sorted(flat, key=keyf), key=keyf):
|
||||||
items = list(it)
|
items = list(it)
|
||||||
grouped.append({
|
grouped.append({
|
||||||
"key": k,
|
"key": k,
|
||||||
"label": _group_label(group_by, k),
|
"label": _group_label(group_by, k),
|
||||||
"status": items[0]["status"],
|
"status": _group_status(items),
|
||||||
"posts": items,
|
"posts": items,
|
||||||
})
|
})
|
||||||
# порядок групп: new → rejected → published (для status), иначе по ключу
|
# порядок групп: new → rejected → published (для status), иначе по ключу
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<title>VESTI — {% block title %}Управление{% endblock %}</title>
|
<title>VESTI — {% block title %}Управление{% endblock %}</title>
|
||||||
<link href="/static/bootstrap.min.css" rel="stylesheet">
|
<link href="/static/bootstrap.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
body { padding-top: 1.5rem; background: #f5f6fa; }
|
body { padding-top: 4.5rem; background: #f5f6fa; }
|
||||||
.card { box-shadow: 0 1px 4px rgba(0,0,0,.08); }
|
.card { box-shadow: 0 1px 4px rgba(0,0,0,.08); }
|
||||||
.post-text { white-space: pre-wrap; max-height: 140px; overflow-y: auto; font-size: .9rem; }
|
.post-text { white-space: pre-wrap; max-height: 140px; overflow-y: auto; font-size: .9rem; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<span class="badge bg-secondary">{{ p.direction or '?' }}</span>
|
<span class="badge bg-secondary">#{{ p.id }} · {{ p.direction or '?' }}</span>
|
||||||
<small class="text-muted">{{ p.source_name }} · {{ p.published_at | dt }}</small>
|
<small class="text-muted">{{ p.source_name }} · {{ p.published_at | dt }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="post-text mt-2">{{ (p.summary or p.text or '')[:2000] | markdown }}</div>
|
<div class="post-text mt-2">{{ (p.summary or p.text or '')[:2000] | markdown }}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user