# healthlog Коллектор данных Apple Health. Принимает выгрузки из [Health Auto Export](https://www.healthyapps.dev/apps/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](docs/architecture.md). ## Состояние В разработке. Готовы каркас и приём, большая часть разбора: сервис принимает пакеты, складывает их в сырой архив и **разбирает метрики в часовые объекты** — с выводом слоя из данных, канонизацией содержимого и слиянием точек по полноте. Тренировки и записи со своим `id` (`workouts`, `stateOfMind`) тоже разбираются; секции, которых разбор не покрывает, принимаются, хранятся и честно помечаются как неразобранные — а имя, которого поток раньше не приносил, даёт `WARN` в логе свёртки один раз и попадает в перечень `healthlog uncovered`. Есть и пересборка: `healthlog reindex` проигрывает журнал доставок в свежую витрину и сверяет её отпечаток с накопленной — на живом архиве из 116 тел пересборка воспроизводима и повторный прогон ничего не меняет. Первый маршрут чтения открыт: **каталог разрезов** (`GET /api/v1/metrics`) под токеном чтения отдаёт слои с диапазонами и измеренный род агрегации, а повтор неизменившегося отвечает `304` по `ETag` — снимок витрины при этом не открывается. Журнал WAL разбирается фоновым чекпойнтом по таймеру. Чего ещё нет: **read API точек**, тренировок и записей — сами данные наружу пока не отдаются. План в [docs/tasks/PLAN.md](docs/tasks/PLAN.md). Разведка формата закончена: 50 находок на живом потоке, половина расходится с документацией Health Auto Export — [docs/research/apple-health.md](docs/research/apple-health.md). ## Команды ``` healthlog serve приём + read API + MCP healthlog import родной экспорт Apple Health (в планах) healthlog reindex пересборка витрины из журнала healthlog uncovered перечень секций, которых разбор не покрыл 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://:8080/api/v1/ingest`. Если `auth.write_tokens` пуст, проверка токена выключена — для доверенной локальной сети этого достаточно, сервис пишет об этом `write auth disabled` на старте. Для доступа снаружи понадобится и токен, и TLS — это шаг «Деплой». ## Документация - [docs/passport.md](docs/passport.md) — цель проекта, типовые сценарии работы, референсы: чужие проекты, у которых смотрим решения, прежде чем придумывать своё - [docs/architecture.md](docs/architecture.md) — устройство: принципы, компоненты, внешние границы, эксплуатация, деплой - [docs/database.md](docs/database.md) — схема хранилища и настройки с числовым значением - [docs/adr/](docs/adr/README.md) — почему решено именно так - [docs/conventions/](docs/conventions/README.md) — как пишем код - [docs/security.md](docs/security.md) — периметр и модель угроз - [docs/review.md](docs/review.md) — настройка конвейера ревью и журнал дефектов - [docs/tasks/PLAN.md](docs/tasks/PLAN.md) — цели и обоснование их порядка - [docs/tasks/BACKLOG.md](docs/tasks/BACKLOG.md) — что брать следующим, включая отложенные идеи - [docs/research/apple-health.md](docs/research/apple-health.md) — что показал реальный поток Health Auto Export; источник истины по формату, документация приложения местами расходится с тем, что оно шлёт