av b2bdb6383f review.md: записан промах — ответ владельца не превращал задачу в берущуюся
- три задачи с решением от 2026-08-02 сохраняли тег question и непустой раздел
  «Вопросы», то есть sprint take отказал бы их взять
- там же названы два числа сессии: ориентир 5–8 задач ничем не замерян, а отбор
  по --stale слеп, пока у каталога нет собственной истории правок
2026-08-03 17:47:29 +03:00

healthlog

Коллектор данных Apple Health. Принимает выгрузки из Health Auto Export, складывает их в единое хранилище и отдаёт другим моим проектам через HTTP API.

Зачем

Данные о здоровье и тренировках нужны сразу трём моим приложениям: агенту-медику (анализ здоровья), трекеру (разбор тренировок) и игре (мотиватор по активности). Интегрировать каждое из них с Health Auto Export по отдельности — значит в каждом писать приём, дедупликацию и хранение заново.

healthlog делает это один раз. Телефон шлёт данные в него, все остальные проекты берут данные из него.

Границы

Это хранилище, а не аналитика. healthlog принимает, дедуплицирует, хранит и отдаёт. Он не переименовывает поля Apple и не интерпретирует значения — этим занимается тот, кто данные читает.

Одну уступку хранилище всё же делает: оно умеет свести метрику к запрошенной сетке («шаги по дням»). Иначе каждый из клиентов повторял бы одну и ту же логику выбора слоя, а ошибиться в ней легко — просуммировать не тот разрез и получить завышение втрое. Но род свёртки не проставлен вручную, а измерен сверкой слоёв между собой; где измерить не вышло, свёртка не предлагается вовсе.

Источников два: Health Auto Export (куплен, пожизненный премиум) — ежедневный поток, и родной экспорт Apple Health раз в 2–3 месяца — источник истины для нижнего слоя.

Как устроено

экспорт Apple ────────┐   снапшот всей истории, раз в 2–3 месяца
                      ▼
iPhone ──HTTPS POST──► healthlog ──► журнал доставок (.json.gz)
                            │              │
                            │              └── события поверх снапшота
                            ▼
                       SQLite ──┬──► HTTP read API ──► мои приложения
                    (свёртка по  │
                     журналу)    └──► MCP ───────────► агенты

Приём сначала кладёт тело запроса на диск как есть и только потом разбирает. Значит, ошибка в разборе не теряет данные: состояние всегда пересобирается свёрткой import(экспорт) + replay(доставки). Отсюда и главный инвариант — точки хранятся дословно: журнал, из которого что-то выброшено, перестаёт быть журналом.

Подробности — docs/architecture.md.

Состояние

В разработке. Готовы каркас и приём, большая часть разбора: сервис принимает пакеты, складывает их в сырой архив и разбирает метрики в часовые объекты — с выводом слоя из данных, канонизацией содержимого и слиянием точек по полноте. Тренировки и записи со своим id (workouts, stateOfMind) тоже разбираются; секции, которых разбор не покрывает, принимаются, хранятся и честно помечаются как неразобранные.

Есть и пересборка: healthlog reindex проигрывает журнал доставок в свежую витрину и сверяет её отпечаток с накопленной — на живом архиве из 116 тел пересборка воспроизводима и повторный прогон ничего не меняет.

Первый маршрут чтения открыт: каталог разрезов (GET /api/v1/metrics) под токеном чтения отдаёт слои с диапазонами и измеренный род агрегации, а повтор неизменившегося отвечает 304 по ETag — снимок витрины при этом не открывается. Журнал WAL разбирается фоновым чекпойнтом по таймеру.

Чего ещё нет: read API точек, тренировок и записей — сами данные наружу пока не отдаются. План в docs/tasks/PLAN.md.

Разведка формата закончена: 50 находок на живом потоке, половина расходится с документацией Health Auto Export — docs/research/apple-health.md.

Команды

healthlog serve        приём + read API + MCP
healthlog import       родной экспорт Apple Health         (в планах)
healthlog reindex      пересборка витрины из журнала
healthlog healthcheck  проверка живости для docker HEALTHCHECK

Пересборка витрины

Разбор пишется по реальным данным и будет ошибаться. Исправленный разбор применяется к уже разобранному пересборкой:

healthlog reindex --config ./config.toml

Команда собирает витрину в отдельный файл рядом с рабочей базой и печатает два отпечатка — рабочей витрины и пересобранной. Рабочую базу она не трогает вовсе (открывает её только на чтение и без наката миграций), поэтому запускать её при живом сервисе безопасно — так и стоит делать, если нужно просто сверить.

Применить результат — другое дело. Подмена возможна только при остановленном сервисе: он держит файл базы открытым, и переименование поверх живого процесса портит базу молча. Сервис при этом надо остановить до пересборки, а не после: доставки, приехавшие за время прогона, в собранный файл не попадут, и подмена стёрла бы их учёт вместе с заголовками, которые не восстанавливаются ниоткуда. Команда это проверяет и в таком случае процедуру подмены не печатает вовсе.

task down
healthlog reindex --config ./config.toml
mv ./data/healthlog.db.rebuild ./data/healthlog.db
rm -f ./data/healthlog.db-wal ./data/healthlog.db-shm
task up

Прогон идёт линейно по архиву: на 116 телах — около полуминуты, и время растёт вместе с архивом. Свободного места нужно не меньше текущего размера базы: собранный файл ложится рядом с ней, на тот же том.

Прогон, убитый жёстко (SIGKILL, потеря питания), оставляет рядом с базой файлы *.partial* — это его недособранный результат. Штатное прерывание (Ctrl+C) их убирает само; оставшиеся можно удалять руками, следующему прогону они не мешают.

Локальный запуск

Конфиг необязателен — без него берутся умолчания (:8080, ./healthlog.db, ./raw). Для своих значений скопируй config.example.toml в config.toml.

task run

Проверка:

curl localhost:8080/healthz
curl -X POST localhost:8080/api/v1/ingest -d '{"data":{"metrics":[]}}'
curl localhost:8080/api/v1/metrics    # каталог: слои, диапазоны, род агрегации

# повтор неизменившегося не стоит ничего: метка из ответа возвращается условием
curl -i -H 'If-None-Match: W/"…"' localhost:8080/api/v1/metrics   # 304

Подключение телефона по локальной сети

Сервис слушает все интерфейсы (addr = ":8080"), так что телефон в той же сети достучится по IP машины. В Health Auto Export заводится одна автоматизация: REST API, формат JSON, минимальная гранулярность («Summarize Data» выключен), URL вида http://<ip-машины>:8080/api/v1/ingest.

Если auth.write_tokens пуст, проверка токена выключена — для доверенной локальной сети этого достаточно, сервис пишет об этом write auth disabled на старте. Для доступа снаружи понадобится и токен, и TLS — это шаг «Деплой».

Документация

  • docs/passport.md — цель проекта, типовые сценарии работы, референсы: чужие проекты, у которых смотрим решения, прежде чем придумывать своё
  • docs/architecture.md — устройство: принципы, компоненты, внешние границы, эксплуатация, деплой
  • docs/database.md — схема хранилища и настройки с числовым значением
  • docs/adr/ — почему решено именно так
  • docs/conventions/ — как пишем код
  • docs/security.md — периметр и модель угроз
  • docs/review.md — настройка конвейера ревью и журнал дефектов
  • docs/tasks/PLAN.md — цели и обоснование их порядка
  • docs/tasks/BACKLOG.md — что брать следующим, включая отложенные идеи
  • docs/research/apple-health.md — что показал реальный поток Health Auto Export; источник истины по формату, документация приложения местами расходится с тем, что оно шлёт
S
Description
No description provided
Readme
2 MiB
Languages
Go 98.2%
Python 1.7%
Dockerfile 0.1%