Files
vesti/openspec/changes/archive-navigation/design.md
T

124 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design — archive-navigation
## Контекст
- Роут: `GET /published` (`web/app.py:735`), шаблон `web/templates/published.html` (42 строки).
- Дата: `posts.published_at TEXT` — ISO 8601 с таймзоной (`2026-09-08T08:02:45+00:00`).
- Сейчас: `LIMIT 100` без пагинации, без фильтров.
- Фронтенд: локальный Bootstrap (`web/static/bootstrap.min.css`), без JS-фреймворков,
htmx запрещён (AGENT.MD). Паттерн фильтра по направлениям — query-параметры (`_cand_back`).
## 1. Хелпер дерева навигации
В `web/app.py`, рядом с `_db()`:
```python
MONTH_NAMES_RU = ("", "Янв", "Фев", "Мар", "Апр", "Май", "Июн",
"Июл", "Авг", "Сен", "Окт", "Ноя", "Дек")
def _archive_tree(conn):
"""Дерево навигации архива: [{year, months:[{m, label, n}], n}] + {no_date:n}.
Строится только из существующих периодов; пустые месяцы отсутствуют."""
rows = conn.execute(
"""SELECT strftime('%Y', published_at) y,
strftime('%m', published_at) m, COUNT(*) n
FROM posts WHERE status='published' AND published_at IS NOT NULL
AND published_at != ''
GROUP BY y, m ORDER BY y DESC, m DESC"""
).fetchall()
years, cur = [], None
for r in rows:
if cur is None or cur["year"] != r["y"]:
cur = {"year": r["y"], "months": [], "n": 0}
years.append(cur)
cur["months"].append({"m": r["m"], "label": f"{MONTH_NAMES_RU[int(r['m'])]} {r['y']}",
"n": r["n"]})
cur["n"] += r["n"]
no_date = conn.execute(
"""SELECT COUNT(*) FROM posts
WHERE status='published' AND (published_at IS NULL OR published_at='')"""
).fetchone()[0]
return {"years": years, "no_date": no_date}
```
- Сортировка: годы и месяцы — по убыванию (свежие сверху, Q9).
- Пустой период исключается самой группировкой `GROUP BY` — ровно как Hugo-архивы
(`site.RegularPages | groupBy "Date.Year"`), где календарь не итерируется (Q8).
## 2. Роут `GET /published`
```python
ARCHIVE_PAGE_SIZE = 10
@app.get("/published", response_class=HTMLResponse)
def published(request: Request, year: str = "", month: str = "", page: int = 1):
_require_auth(request)
page = max(1, page)
conn = _db()
tree = _archive_tree(conn)
where, params = ["p.status='published'"], []
if year:
where.append("strftime('%Y', p.published_at) = ?"); params.append(year)
if month:
where.append("strftime('%m', p.published_at) = ?"); params.append(month)
# год без месяца = все посты за год; "Без даты" не попадает в year/month
if not year and not month:
where.append("(p.published_at IS NOT NULL AND p.published_at != '')")
total = conn.execute(f"SELECT COUNT(*) FROM posts p WHERE {' AND '.join(where)}",
params).fetchone()[0]
pages = max(1, (total + ARCHIVE_PAGE_SIZE - 1) // ARCHIVE_PAGE_SIZE)
page = min(page, pages)
rows = conn.execute(
f"""SELECT p.*, s.name source_name,
pub.tg_message_id, pub.views pub_views, pub.bundle_path, pub.distributed_dirs
FROM posts p
LEFT JOIN sources s ON s.id=p.source_id
LEFT JOIN published pub ON pub.post_id=p.id
WHERE {' AND '.join(where)}
ORDER BY p.published_at DESC LIMIT ? OFFSET ?""",
params + [ARCHIVE_PAGE_SIZE, (page - 1) * ARCHIVE_PAGE_SIZE]
).fetchall()
conn.close()
html = tpl.get_template("published.html").render(
posts=rows, tree=tree, year=year, month=month,
page=page, pages=pages, total=total,
)
return HTMLResponse(html)
```
- Блок «Без даты» показывается как ссылка `/?year=&month=`? Нет — отдельный
фильтр: кнопка «Без даты» (query `nodate=1`), тоже с деревом и пагинацией.
(Добавлено в spec ниже — сценарий «Без даты»).
## 3. Шаблон `published.html`
- Обёртка: `row` → `col-lg-9` (список) + `col-lg-3` (дерево, `position-sticky top-2`).
- Дерево:
- ссылка «📄 Все посты» → `/published` (активна, если нет year/month/nodate).
- по годам: `<h6>2026 ▾</h6>` + `<ul>` месяцев; месяц — ссылка
`/published?year=2026&month=09`, счётчик `(4)`. Активный месяц подсвечен
(`active-row`-стиль, как в кандидатах).
- на мобильных: дерево в `collapse` (кнопка «🗂 Архив» + `data-bs-toggle`).
- Список: карточки как сейчас, но посты только за выбранный период.
- Пагинация снизу: Bootstrap `pagination`, `page-item active` для текущей,
prev/next, номера 1..pages (с сохранением year/month/nodate).
- Пустой период: `alert-info` «Нет постов за этот период» (дерево всё равно видно).
## 4. Пагинация
- Bootstrap HTML, без JS: `?year=2026&month=09&page=N`.
- Текущая страница — `active`, prev/next — disabled на границах.
- total = число постов в периоде; pages = ceil(total/10).
## 5. Проверка
- `openspec validate archive-navigation` — чисто.
- `curl -s 'http://127.0.0.1:8400/published?year=2026&month=09'` — видно 4 поста.
- `curl -s 'http://127.0.0.1:8400/published?month=07'` — 2 поста.
- В HTML дерева нет месяца без постов (только 07, 08, 09) — «февраль отсутствует».
- `?page=1`..N — пагинация с номерами; вне диапазона — клампится к последней.
- Мобильная вёрстка: сужение окна → колонка сворачивается.