- род метрики выводится сверкой минутного слоя с часовым: часовое значение сходится с суммой минутных — накопительная, со средним — мгновенная, иначе `unknown` и свёртка не предлагается вовсе. На живом архиве (123 доставки, 31 метрика) 7 накопительных, 9 мгновенных, противоречащих часов ноль - `GET /api/v1/metrics` под токеном чтения отдаёт единицы, слои с границами и род вместе с основанием измерения; род нигде не хранится — он функция витрины, а витрина функция журнала, устаревать в нём нечему - миграция 00009: покрывающий индекс, чтобы каталог отвечал по учётным колонкам, не разжимая содержимое объектов
162 lines
10 KiB
Markdown
162 lines
10 KiB
Markdown
# 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).
|
||
|
||
## Состояние
|
||
|
||
В разработке. Готовы каркас и приём, большая часть разбора: сервис принимает
|
||
пакеты, складывает их в сырой архив и **разбирает метрики в часовые объекты**
|
||
— с выводом слоя из данных, канонизацией содержимого и слиянием точек по
|
||
полноте. Секции, которых разбор пока не покрывает (`workouts`, `stateOfMind` —
|
||
половина потока), принимаются, хранятся и честно помечаются как неразобранные.
|
||
|
||
Есть и пересборка: `healthlog reindex` проигрывает журнал доставок в свежую
|
||
витрину и сверяет её отпечаток с накопленной — на живом архиве из 116 тел
|
||
пересборка воспроизводима и повторный прогон ничего не меняет.
|
||
|
||
Чего ещё нет: каталога метрик с измеренным родом агрегации и **read API** —
|
||
данные наружу пока не отдаются никак. План в [docs/plan.md](docs/plan.md).
|
||
|
||
Разведка формата закончена: 50 находок на живом потоке, половина расходится с
|
||
документацией Health Auto Export — [docs/local-research.md](docs/local-research.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 # каталог: слои, диапазоны, род агрегации
|
||
```
|
||
|
||
### Подключение телефона по локальной сети
|
||
|
||
Сервис слушает все интерфейсы (`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/passport.md) — цель проекта, типовые сценарии
|
||
работы, референсы: чужие проекты, у которых смотрим решения, прежде чем
|
||
придумывать своё
|
||
- [docs/architecture.md](docs/architecture.md) — устройство, схема данных, API, принятые решения
|
||
- [docs/conventions.md](docs/conventions.md) — как пишем код
|
||
- [docs/plan.md](docs/plan.md) — шаги и обоснование их порядка
|
||
- [docs/backlog](docs/backlog/README.md) — что брать следующим, включая
|
||
отложенные идеи
|
||
- [docs/local-research.md](docs/local-research.md) — что показал реальный поток
|
||
Health Auto Export; источник истины по формату, документация приложения
|
||
местами расходится с тем, что оно шлёт
|