## Why

Первый читающий маршрут (`GET /api/v1/metrics`) обошёлся дороже, чем выглядел:
два прохода ревью измерили 693 мс и +153 МиБ живой кучи на враждебном запросе,
а непрерывная запись вместе с четырьмя читающими транзакциями внахлёст дала
рост `-wal` около 7 МБ/с без верхней границы (40 МБ за пять секунд). Приём
живёт в том же процессе, и обе цены платит он: OOM убивает приём, а доставка,
не попавшая в архив, телефоном не переприсылается. Третье проявление той же
причины — повтор: спека каталога уже требует побайтового совпадения двух
ответов на неизменившейся витрине, то есть ресурс по построению пригоден для
условного запроса, а `ETag` не выставляется вовсе.

Задача берётся **перед** Read API точек намеренно: тот строится поверх этой же
машинерии, и решать один вопрос трижды (каталог, точки, MCP) нельзя.

## What Changes

- **Периодический чекпойнт WAL.** Рядом с воркером свёртки живёт горутина,
  которая раз в минуту выполняет `PRAGMA wal_checkpoint(PASSIVE)` и
  останавливается дренированием, как воркер. Автоматический чекпойнт SQLite
  срабатывает только по концу записи, поэтому WAL, раздутый всплеском, остаётся
  неразобранным до следующей доставки — а ночью телефон молчит часами.
- **Наблюдаемость непродвинувшегося чекпойнта.** Пассивный чекпойнт не идёт
  дальше снимка самого старого активного читателя и **ошибки при этом не
  возвращает**: измерено — `busy=0`, `log=6256`, `checkpointed=5`. Значит
  единственный различимый признак — «страниц в журнале много, перенесено
  меньше», и именно он идёт в `WARN` владельцу.
- **Названный предел файла журнала.** `journal_size_limit` в строке
  подключения: пассивный чекпойнт возвращает страницы в базу, но файл оставляет
  на пике (измерено: 51 МБ до и после успешного чекпойнта на 12502 страницы).
  Роста это не ограничивает — усечение делает первая запись после полного
  чекпойнта, — и так и сказано в спеке.
- **Версия витрины и условный запрос.** Хранилище отдаёт версию витрины по
  `PRAGMA data_version`, каталог выставляет `ETag`, а на `If-None-Match` с
  непротухшей версией отвечает `304` **не открывая снимок вовсе**.
- **Не делается** (отложено): предел размера ответа и собственный дедлайн
  маршрута — их проектирует Read API точек; потоковое измерение по метрике;
  кеш ответа; `HEAD` на маршруте каталога.

## Capabilities

### New Capabilities

Новых нет: обе части ложатся на существующие домены.

### Modified Capabilities

- `storage`: добавляется **версия витрины** (признак изменения «в базу никто не
  коммитил»; монотонной она не является) и **обслуживание WAL** (чекпойнт по
  таймеру, признак непродвижения, остановка дренированием).
- `catalog`: добавляется **условный запрос** — `ETag` на ответе каталога и
  `304` на `If-None-Match`, связанный с уже существующим требованием
  побайтового совпадения двух ответов на неизменившейся витрине.

## Impact

- `internal/store` — закреплённое соединение-щуп для `data_version`, метод
  чекпойнта WAL, `journal_size_limit` в DSN.
- `internal/catalog` — снимок каталога уезжает вместе с версией витрины.
- `internal/httpapi` — общий помощник условного запроса (им же будут
  пользоваться точки и MCP), `ETag`/`304` на маршруте каталога.
- `cmd/healthlog/serve.go` — горутина чекпойнта и её дренирование в общем
  бюджете остановки.
- Схема базы **не меняется**: миграции нет.
- Контракт приёма не меняется. Контракт чтения расширяется совместимо: клиент,
  не присылающий `If-None-Match`, получает ровно то же, что и сегодня.
