From 59ec61318446b8643dfaca2d5a8f1d281a0d221c Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Sat, 1 Aug 2026 14:11:42 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B7=D0=B0=D0=B2=D0=B5=D0=B4=D1=91=D0=BD=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=BA=D0=BB=D0=BE=D0=B3=20=D0=BF=D1=80=D0=BE=D0=B5?= =?UTF-8?q?=D0=BA=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 20 задач: 8 высоких, 9 средних, 3 низких; две заведены идеями — сутки при смене часового пояса и пересекающиеся источники одной метрики - план сведён к порядку и его обоснованию, единицы работы переехали в беклог --- CLAUDE.md | 26 ++++++++++++++- docs/backlog/CLOSED.md | 5 +++ docs/backlog/README.md | 32 +++++++++++++++++++ docs/backlog/alert-tishina-potoka.md | 17 ++++++++++ docs/backlog/bekap-dannyh.md | 17 ++++++++++ docs/backlog/deploy-rivendell.md | 22 +++++++++++++ docs/backlog/import-eksporta-apple.md | 22 +++++++++++++ docs/backlog/mcp-server.md | 23 +++++++++++++ docs/backlog/openapi-swagger.md | 24 ++++++++++++++ .../backlog/peresekayushchiesya-istochniki.md | 20 ++++++++++++ docs/backlog/proverka-novyh-sekcij.md | 21 ++++++++++++ docs/backlog/razbor-metrik-v-obekty.md | 31 ++++++++++++++++++ docs/backlog/read-api-tochki.md | 21 ++++++++++++ docs/backlog/reindex-iz-arhiva.md | 16 ++++++++++ docs/backlog/retenshen-syrogo-arhiva.md | 14 ++++++++ docs/backlog/rod-agregacii-i-katalog.md | 21 ++++++++++++ docs/backlog/samoopisanie-shemy.md | 21 ++++++++++++ .../backlog/slovar-kategorialnyh-znachenij.md | 22 +++++++++++++ docs/backlog/stats-nablyudaemost.md | 18 +++++++++++ docs/backlog/sutki-i-chasovoj-poyas.md | 20 ++++++++++++ docs/backlog/trenirovki-i-zapisi.md | 23 +++++++++++++ docs/backlog/upravlenie-sekretami.md | 22 +++++++++++++ docs/backlog/ustarevanie-nizhnego-sloya.md | 22 +++++++++++++ docs/plan.md | 4 +++ 24 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 docs/backlog/CLOSED.md create mode 100644 docs/backlog/README.md create mode 100644 docs/backlog/alert-tishina-potoka.md create mode 100644 docs/backlog/bekap-dannyh.md create mode 100644 docs/backlog/deploy-rivendell.md create mode 100644 docs/backlog/import-eksporta-apple.md create mode 100644 docs/backlog/mcp-server.md create mode 100644 docs/backlog/openapi-swagger.md create mode 100644 docs/backlog/peresekayushchiesya-istochniki.md create mode 100644 docs/backlog/proverka-novyh-sekcij.md create mode 100644 docs/backlog/razbor-metrik-v-obekty.md create mode 100644 docs/backlog/read-api-tochki.md create mode 100644 docs/backlog/reindex-iz-arhiva.md create mode 100644 docs/backlog/retenshen-syrogo-arhiva.md create mode 100644 docs/backlog/rod-agregacii-i-katalog.md create mode 100644 docs/backlog/samoopisanie-shemy.md create mode 100644 docs/backlog/slovar-kategorialnyh-znachenij.md create mode 100644 docs/backlog/stats-nablyudaemost.md create mode 100644 docs/backlog/sutki-i-chasovoj-poyas.md create mode 100644 docs/backlog/trenirovki-i-zapisi.md create mode 100644 docs/backlog/upravlenie-sekretami.md create mode 100644 docs/backlog/ustarevanie-nizhnego-sloya.md diff --git a/CLAUDE.md b/CLAUDE.md index 580deb1..15b9630 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,12 +56,36 @@ Module path — `git.vakhrushev.me/av/healthlog`. Запуск через [Task](https://taskfile.dev) (`task --list` — полный список): -- `task run` — локальный запуск (`--config ./config.toml`) +- `task up` / `task restart` / `task down` — сервис в контейнере; **основной + способ запуска**, данные в `./data` переживают пересборку +- `task logs` / `task ps` — что происходит с сервисом +- `task gate` — детерминированный гейт ревью (build/vet/lint/test/race/ + покрытие диффа/миграции/образцы конфига/секреты/данные в индексе) +- `task review:context` — вход для архитектурного прохода ревью +- `task run` — запуск из исходников, без контейнера - `task build` — статический бинарь linux/amd64 - `task test` / `task lint` — тесты и golangci-lint - `task tidy` — `go mod tidy` - `task setup` — установка golangci-lint +## Процесс + +Задачи — в [docs/backlog](docs/backlog/README.md) (один файл на задачу, индекс +производен). Порядок и его обоснование — в [docs/plan.md](docs/plan.md). + +Работа над задачей идёт скиллом `task-pipeline`: беклог → `opsx:explore` → +`opsx:propose` → ревью спек (профиль `design`) → `opsx:apply` → ревью кода → +`opsx:archive` → чистка беклога → коммит. Ревью — скилл `review-pipeline`, +проходы — агенты `healthlog-review-*`. + +Гейт блокирует: пока `task gate` красный, опиниативные проходы ревью не +запускаются. + +**Поток не останавливается.** Телефон шлёт непрерывно и молча. Сломанный приём, +оставленный работать, теряет данные необратимо — сырой архив живёт 14 дней. +Ничего из `./data` не попадает ни в git, ни в логи выше `DEBUG`, ни в вывод +агента. + ## Конвенции Механизируемое проверяет `task lint` (`.golangci.yml`): форма логов diff --git a/docs/backlog/CLOSED.md b/docs/backlog/CLOSED.md new file mode 100644 index 0000000..e36ff85 --- /dev/null +++ b/docs/backlog/CLOSED.md @@ -0,0 +1,5 @@ +# Кладбище беклога + +Задачи, покинувшие беклог без реализации. Пишется `backlog.py close`. + + diff --git a/docs/backlog/README.md b/docs/backlog/README.md new file mode 100644 index 0000000..80acf24 --- /dev/null +++ b/docs/backlog/README.md @@ -0,0 +1,32 @@ +# Беклог + +Одна задача = один файл `.md` + строка в этом индексе. +Приоритет — грубая оценка «ценность / стоимость». Спекулятивные +задачи помечены `[idea]` в заголовке. Ведётся скиллом `backlog`. + +## высокий +- [Разбор метрик в часовые объекты](razbor-metrik-v-obekty.md) — Доставки копятся непрозрачными телами — точек в хранилище нет вовсе, всё остальное упирается в это +- [Тренировки и секции с собственными id](trenirovki-i-zapisi.md) — Тренировки с геотреком и состояние разума приходят, но не разбираются — без них не закрыть ни трекер, ни агента-медика +- [Пересборка хранилища из сырого архива](reindex-iz-arhiva.md) — Разбор будет ошибаться, а окно на исправление — 14 дней жизни архива +- [Измеренный род агрегации и каталог разрезов](rod-agregacii-i-katalog.md) — Без рода метрики свёртка в ответе неотличима от угадывания — а суммировать нижний слой значит завысить втрое +- [Read API: точки, выбор слоя, свёртка по сетке](read-api-tochki.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может +- [OpenAPI-спека и Swagger UI](openapi-swagger.md) — Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате +- [MCP-сервер поверх Read API](mcp-server.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем +- [Резервное копирование ./data](bekap-dannyh.md) — История здоровья существует в единственном экземпляре на рабочей машине — потеря каталога необратима + +## средний +- [Словарь категориальных значений → коды HealthKit](slovar-kategorialnyh-znachenij.md) — Фазы сна и типы тренировок приходят строками русской локали — с экспортом Apple их не сверить +- [Выведенные из данных схемы содержимого](samoopisanie-shemy.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке +- [Импорт родного экспорта Apple Health](import-eksporta-apple.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут +- [Проверка секций, которых поток ещё не приносил](proverka-novyh-sekcij.md) — Симптомы, ЭКГ, лекарства, цикл и вес не виденны живьём — разбор писался вслепую +- [Наблюдаемость: /stats](stats-nablyudaemost.md) — Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах +- [Деплой на rivendell](deploy-rivendell.md) — Сервис живёт на рабочей машине — телефон достаёт до него только дома +- [Управление токенами и секретами](upravlenie-sekretami.md) — Проверка токенов выключена, config.docker.toml коммитится — так нельзя выезжать наружу +- [[idea] Что считать сутками при смене часового пояса](sutki-i-chasovoj-poyas.md) — Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено +- [[idea] Пересекающиеся источники одной метрики](peresekayushchiesya-istochniki.md) — Сон пишут и часы, и стороннее приложение — сумма по обоим задвоит ночь + +## низкий +- [Устаревание нижнего слоя после экспорта](ustarevanie-nizhnego-sloya.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен +- [Ретеншен сырого архива](retenshen-syrogo-arhiva.md) — Архив не подчищается вовсе — 14-дневный срок объявлен, но не работает +- [Активный алерт «данных нет N часов»](alert-tishina-potoka.md) — Пропажу потока сейчас замечает человек, а не сервис + diff --git a/docs/backlog/alert-tishina-potoka.md b/docs/backlog/alert-tishina-potoka.md new file mode 100644 index 0000000..7fff7a7 --- /dev/null +++ b/docs/backlog/alert-tishina-potoka.md @@ -0,0 +1,17 @@ +# Активный алерт «данных нет N часов» + +**Приоритет:** низкий + +Пропажу потока сейчас замечает человек. `/stats` покажет факт, но только если +туда заглянуть — а заглядывают ровно тогда, когда уже что-то заподозрили. + +Активное уведомление закрывает разрыв: сервис сам сообщает, что данных нет +дольше порога. Канал — тот же, что у остальных моих проектов. + +Порог не единый: быстрый проход идёт каждые 5 минут, но ночью телефон +заблокирован и тишина штатна (находка 28). Значит порог считается по времени +суток или по последней успешной доставке каждой автоматизации отдельно. + +Приоритет низкий, пока сервис на рабочей машине и я вижу его каждый день. +После деплоя на rivendell поднимется. + diff --git a/docs/backlog/bekap-dannyh.md b/docs/backlog/bekap-dannyh.md new file mode 100644 index 0000000..1e3f0e0 --- /dev/null +++ b/docs/backlog/bekap-dannyh.md @@ -0,0 +1,17 @@ +# Резервное копирование ./data + +**Приоритет:** высокий + +`./data` — это база и 16 МБ сырого архива в единственном экземпляре на рабочей +машине. Родной экспорт Apple делается раз в 2–3 месяца, то есть потеря каталога +стоит всей истории с момента последнего экспорта. + +Отдельная тонкость: SQLite нельзя копировать `cp` под нагрузкой, а телефон шлёт +непрерывно — нужен `VACUUM INTO` или backup API, а не копирование файла. + +Приоритет высокий не по ценности, а по асимметрии: задача на пару часов +страхует данные, которые иначе не восстановить ничем. + +Готово, когда есть команда снятия копии, она отрабатывает на живой базе под +приёмом, и копия проверяется восстановлением хотя бы раз. + diff --git a/docs/backlog/deploy-rivendell.md b/docs/backlog/deploy-rivendell.md new file mode 100644 index 0000000..2de599c --- /dev/null +++ b/docs/backlog/deploy-rivendell.md @@ -0,0 +1,22 @@ +# Деплой на rivendell + +**Приоритет:** средний + +Сервис живёт в контейнере на рабочей машине, телефон достаёт до него только +дома. Вне дома экспорт копится и уезжает пачкой при возвращении — работает, но +это не то, ради чего заводился всегда доступный VPS. + +Есть готовый образец: jellybit собирает образ локально и отправляет на сервер +через `docker save`/`load`, Caddy впереди терминирует TLS. Здесь то же самое. + +Шаги: +- сборка образа локально, доставка на rivendell; +- Caddy: поддомен приёма и поддомен чтения (плюс MCP на нём же); +- тома под `./data`, конфиг с токенами отдельно, права `0600`. + +Готово, когда телефон шлёт на публичный адрес из любой сети, а агент читает по +тому же домену. + +Зависит от задачи про секреты: выезжать наружу с выключенной проверкой токенов +нельзя. + diff --git a/docs/backlog/import-eksporta-apple.md b/docs/backlog/import-eksporta-apple.md new file mode 100644 index 0000000..1b52e89 --- /dev/null +++ b/docs/backlog/import-eksporta-apple.md @@ -0,0 +1,22 @@ +# Импорт родного экспорта Apple Health + +**Приоритет:** средний + +Слой `sample` пуст: настоящих сэмплов HealthKit в потоке нет вовсе — HAE отдаёт +посекундную развёртку, а не измерения (находка 34). Полная история и точные +сэмплы лежат в zip родного экспорта. + +Это же основание для устаревания нижнего слоя и единственный способ поднять +историю глубже недели: дыра старше недели проходами синхронизации не чинится. + +Шаги: +- разбор `экспорт.xml` (HealthKit Export Version 14, `` с + `startDate`/`endDate`/`value`/`sourceName`/`device`) в слой `sample`; +- маршруты GPX и ЭКГ отдельными CSV — они не в XML; +- заливка кусками по годам: файл измеряется сотнями мегабайт. + +Готово, когда история за несколько лет лежит в слое `sample`, а суммы по нему +сходятся с часовым слоем HAE на пересечении периодов. + +Есть готовый файл для проверки: `/home/av/MediaEverything/HealthData/apple_health/`. + diff --git a/docs/backlog/mcp-server.md b/docs/backlog/mcp-server.md new file mode 100644 index 0000000..4bd9355 --- /dev/null +++ b/docs/backlog/mcp-server.md @@ -0,0 +1,23 @@ +# MCP-сервер поверх Read API + +**Приоритет:** высокий + +Агент-медик — первый заказчик проекта. Ему нужна актуальная сводка, а не срез +на дату последнего ручного экспорта. + +Транспорт — **Streamable HTTP**, не stdio: сервис живёт на VPS, агент ходит по +сети. Отсюда: MCP — эндпоинт того же процесса и того же порта, аутентификация — +тот же токен чтения, что у Read API. Отдельного контура доступа не заводим: +MCP не даёт ничего, чего не даёт HTTP, и права обязаны совпадать. + +Инструментов три: каталог разрезов, значения за период, значения с разбивкой. +Собственной логики в адаптере нет. + +Правило размера ответа здесь не украшение, а необходимость: у сетевого агента +нет способа «посмотреть поближе» иначе, чем повторным вызовом. + +Готово, когда агент подключается по URL и отвечает на «как я спал на прошлой +неделе» без промежуточного кода. + +Связано: `docs/architecture.md` → «MCP», план шаг 7. + diff --git a/docs/backlog/openapi-swagger.md b/docs/backlog/openapi-swagger.md new file mode 100644 index 0000000..71dbd26 --- /dev/null +++ b/docs/backlog/openapi-swagger.md @@ -0,0 +1,24 @@ +# OpenAPI-спека и Swagger UI + +**Приоритет:** высокий + +Потребителей три, и один из них — агент, который читает контракт машиной. +Пересказывать форму ответа в чате не годится, а самоописание из плана покрывает +только **содержимое** метрик; форма конверта, коды ответов и параметры запроса — +это OpenAPI. + +Заодно снимает вопрос, чем быть «схеме контракта API» из раздела самоописания: +ею и будет OpenAPI-документ, а не собственный формат. + +Шаги: +- спека OpenAPI 3.1 на приём, каталог, точки, тренировки, записи, `/stats`; +- Swagger UI на отдельном пути, отдаётся самим сервисом (без внешних CDN — + он должен работать в локальной сети без интернета); +- проверка актуальности спеки в гейте: контракт разъезжается молча. + +Готово, когда по спеке можно сгенерировать клиент, а Swagger UI открывается +локально и выполняет запрос к живому сервису. + +Развилка на решение: спека пишется руками как источник истины или выводится из +кода. Для маленького API рукописная спека честнее — но это стоит обсудить. + diff --git a/docs/backlog/peresekayushchiesya-istochniki.md b/docs/backlog/peresekayushchiesya-istochniki.md new file mode 100644 index 0000000..52f34d1 --- /dev/null +++ b/docs/backlog/peresekayushchiesya-istochniki.md @@ -0,0 +1,20 @@ +# [idea] Пересекающиеся источники одной метрики + +**Приоритет:** средний + +Одну метрику пишут несколько источников: сон — часы и стороннее приложение +AutoSleep, шаги — часы и телефон одновременно. Поле `source` при этом не +источник, а множество вкладчиков (`Apple Watch Ultra 3|iPhone (Anton)`), и +состав меняется от группировки (находка 36). + +По координатному ключу столкновений почти нет — источники пишут в разные метки. +Но по **времени** интервалы пересекаются, и сумма по обоим задвоит ночь сна или +дневные шаги. + +Почему идея: неясно, чья это ответственность. Варианты — отдавать как есть и +предупреждать в каталоге, выбирать источник по приоритету, отдавать разбивку по +источникам отдельным разрезом. Первое честнее всего, третье полезнее всего. + +Для агента-медика вопрос практический: «сколько я спал» не должно давать +двойной ответ. + diff --git a/docs/backlog/proverka-novyh-sekcij.md b/docs/backlog/proverka-novyh-sekcij.md new file mode 100644 index 0000000..92f40d3 --- /dev/null +++ b/docs/backlog/proverka-novyh-sekcij.md @@ -0,0 +1,21 @@ +# Проверка секций, которых поток ещё не приносил + +**Приоритет:** средний + +Разбор пишется по тем данным, что видел поток, а он приносил только `metrics`, +`workouts` и `stateOfMind`. Не виденны живьём: `symptoms`, `ecg`, +`heartRateNotifications`, `cycleTracking`, `medications`, а также вес — а вес +агенту-медику нужен наверняка. + +Пользователь настраивает оставшиеся метрики на телефоне, так что данные +появятся сами. Задача — не пропустить момент: убедиться, что новые секции +разбираются, а не молча падают в `parse_status`. + +Отдельный вопрос, на который ответят эти же данные: есть ли эти секции в родном +экспорте Apple. Если нет — экспорт им не источник истины, и устаревание +нижнего слоя к ним неприменимо, держим всегда. + +Готово, когда каждая новая секция либо разобрана, либо явно описана в +`docs/local-research.md` как не пришедшая, и ни одна не числится в ошибках +разбора. + diff --git a/docs/backlog/razbor-metrik-v-obekty.md b/docs/backlog/razbor-metrik-v-obekty.md new file mode 100644 index 0000000..ae6ac0d --- /dev/null +++ b/docs/backlog/razbor-metrik-v-obekty.md @@ -0,0 +1,31 @@ +# Разбор метрик в часовые объекты + +**Приоритет:** высокий + +Приём работает, но дальше архива данные не идут: 89 доставок лежат телами +`.json.gz`, а в SQLite только строки `delivery`. Всё остальное — каталог, +Read API, MCP — стоит на этой задаче. + +Правила выведены на живом потоке и проверены, изобретать заново не нужно +(`docs/local-research.md`, находки 33, 35, 36, 38, 39, 41): + +- ключ точки — координаты `метрика + слой + метка`; `source` в ключ не входит; +- слой выводится из выравнивания меток: плотная метрика (≥10 точек) + классифицируется сама, редкая наследует преобладающий слой доставки; +- при столкновении выигрывает более полная точка, а не последняя пришедшая + (0.66% координат различаются набором полей, а не значением); +- три формата времени: локальное со смещением, RFC 3339 Z, Unix-эпоха внутри + `heartbeatSeries`; +- `sleep_analysis` разводится на два имени — поэпизодное и суточную сводку. + +Шаги: +- миграция `bucket` (`метрика + слой + час`, payload gzip-BLOB, `sealed`); +- разбор метрик, вывод слоя, канонизация с округлением до ~12 значащих цифр; +- слияние точек в объект read-modify-write, хеш объекта как детектор изменений; +- `docs/database.md` — ER-схема (её требует шаг гейта `er-schema`). + +Готово, когда по существующим 89 доставкам собирается хранилище, а суммы по +часовому слою сходятся с проверкой из `tmp/research/`. + +Связано: `docs/architecture.md` → «Хранилище», план шаг 3. + diff --git a/docs/backlog/read-api-tochki.md b/docs/backlog/read-api-tochki.md new file mode 100644 index 0000000..77ed9af --- /dev/null +++ b/docs/backlog/read-api-tochki.md @@ -0,0 +1,21 @@ +# Read API: точки, выбор слоя, свёртка по сетке + +**Приоритет:** высокий + +Сейчас данные достаются только `sqlite3` на хосте. Все три сценария — +агент-медик, трекер тренировок, фитнес-игра — упираются в отсутствие чтения. + +Формы запроса ровно две, и это один запрос с необязательным параметром: +`?from&to` — все значения за период (вес, лекарства, симптомы), `?from&to&bucket` +— с разбивкой (шаги, энергия). + +Решение по размеру ответа (вариант «б»): разбивка не задана и ответ не влезает — +сервер сам берёт сетку погрубее и **называет её в ответе**; разбивка задана явно +и не влезает — ошибка со списком доступных сеток, а не тихая подмена. Различие +существенно: иначе агент, попросивший минутную сетку, получит суточные суммы. + +Готово, когда «шаги за неделю по дням» и «вес за год» отвечаются одним запросом +каждый, а в ответе всегда видно `layer`, `bucket` и `aggregation`. + +Связано: `docs/architecture.md` → «Read API», план шаг 5. + diff --git a/docs/backlog/reindex-iz-arhiva.md b/docs/backlog/reindex-iz-arhiva.md new file mode 100644 index 0000000..89fc2b3 --- /dev/null +++ b/docs/backlog/reindex-iz-arhiva.md @@ -0,0 +1,16 @@ +# Пересборка хранилища из сырого архива + +**Приоритет:** высокий + +Разбор пишется по реальным данным и будет ошибаться — это норма, а не риск. +Риск в другом: сырой архив живёт 14 дней, и окно на исправление ошибки равно +этому сроку. Без `reindex` ошибка разбора становится потерей данных. + +Пересчёт по всей истории сразу ещё и **точнее** приёма: вывод слоя и род +агрегации на полном ряду доставок надёжнее, чем на одной. + +Готово, когда `healthlog reindex` пересобирает хранилище с нуля из `data/raw` +и результат совпадает с накопленным приёмом. + +Связано: план шаг 3, `docs/architecture.md` → «Сырой архив». + diff --git a/docs/backlog/retenshen-syrogo-arhiva.md b/docs/backlog/retenshen-syrogo-arhiva.md new file mode 100644 index 0000000..74eb87c --- /dev/null +++ b/docs/backlog/retenshen-syrogo-arhiva.md @@ -0,0 +1,14 @@ +# Ретеншен сырого архива + +**Приоритет:** низкий + +Срок жизни сырого архива объявлен (14 дней, `storage.raw_retention`), но +удаления нет: архив растёт бесконечно. Пока это 16 МБ и проблемой не является. + +Включать **после** того, как разбор устоится и `reindex` докажет, что +хранилище действительно пересобирается: иначе страховка исчезнет раньше, чем +перестанет быть нужна. + +Готово, когда старые тела удаляются по расписанию, а `/stats` показывает +глубину архива в днях. + diff --git a/docs/backlog/rod-agregacii-i-katalog.md b/docs/backlog/rod-agregacii-i-katalog.md new file mode 100644 index 0000000..f11979a --- /dev/null +++ b/docs/backlog/rod-agregacii-i-katalog.md @@ -0,0 +1,21 @@ +# Измеренный род агрегации и каталог разрезов + +**Приоритет:** высокий + +Решено (вариант «б» груминга): свёртка живёт в ответе, но род метрики +**измеряется**, а не размечается руками. Форма точки рода не выдаёт — +`Avg`/`Min`/`Max` есть только у `heart_rate`, всё остальное приходит в `qty` +(находка 40). Единицы дают процентов девяносто и ломаются на краях. + +Метод: одна метрика лежит в минутном и часовом разрезе одновременно. Часовое +значение сходится с суммой минутных — накопительная; со средним — мгновенная; +данных не хватило — `unknown`, и свёртка по такой метрике не предлагается вовсе. + +Жёсткое правило: накопительные метрики никогда не сворачиваются из нижнего слоя +HAE. Он не сэмплы, а посекундная развёртка (находка 34), сумма по нему завышена. + +Готово, когда каталог отдаёт по каждой метрике единицы, род и список слоёв с +диапазонами, а род проставлен измерением на живой истории. + +Связано: `docs/architecture.md` → «Слои гранулярности», план шаг 4. + diff --git a/docs/backlog/samoopisanie-shemy.md b/docs/backlog/samoopisanie-shemy.md new file mode 100644 index 0000000..8f22531 --- /dev/null +++ b/docs/backlog/samoopisanie-shemy.md @@ -0,0 +1,21 @@ +# Выведенные из данных схемы содержимого + +**Приоритет:** средний + +Метрик у Apple больше сотни, формы точек разные, и рукописный каталог описывал +бы документацию HAE, а не то, что он реально прислал. Схема содержимого +**выводится из данных** тем же проходом разбора: незнакомая метрика описывает +себя сама, без релиза. + +Схема отдаётся вместе со статистикой — для потребителя она важнее формального +типа: сколько точек, первая и последняя метка, единицы, доля присутствия поля. + +Вывод ограничивается по глубине вложенности, иначе схема тренировки с маршрутом +разрастётся до размеров самих данных. + +Готово, когда клиент по `/api/v1/metrics/{name}/schema` видит поля, их типы и +присутствие, не выкачивая выборку. + +Связано: `docs/architecture.md` → «Самоописание». Форму конверта API описывает +не эта задача, а OpenAPI. + diff --git a/docs/backlog/slovar-kategorialnyh-znachenij.md b/docs/backlog/slovar-kategorialnyh-znachenij.md new file mode 100644 index 0000000..c61dae6 --- /dev/null +++ b/docs/backlog/slovar-kategorialnyh-znachenij.md @@ -0,0 +1,22 @@ +# Словарь категориальных значений → коды HealthKit + +**Приоритет:** средний + +HAE отдаёт перечислимые значения строками локали телефона: «БДГ», «Сидячий +образ жизни», «В помещении Ходьба». Родной экспорт Apple при этом говорит +кодами (`HKCategoryValueSleepAnalysisAsleepREM`) — источники несопоставимы +(находка 37). + +Три следствия, и третье решающее: клиент угадывает словарь; смена языка +телефона молча расколет историю; сверить покрытие экспортом нечем — а на этой +сверке стоит устаревание нижнего слоя. + +Решение (вариант «б»): строка хранится **дословно**, рядом кладётся выведенный +код. Словарь ключуется парой `(локаль, строка)`, локаль берётся из +`Accept-Language`. Незнакомая строка → пустой код, а не догадка. + +Готово, когда фазы сна из потока и из экспорта Apple сравниваются напрямую, а +`/stats` показывает строки, для которых кода ещё нет. + +`stateOfMind` в словаре не нуждается — он и так шлёт коды HealthKit. + diff --git a/docs/backlog/stats-nablyudaemost.md b/docs/backlog/stats-nablyudaemost.md new file mode 100644 index 0000000..0c78ca4 --- /dev/null +++ b/docs/backlog/stats-nablyudaemost.md @@ -0,0 +1,18 @@ +# Наблюдаемость: /stats + +**Приоритет:** средний + +Тихо сломавшаяся автоматизация — главный эксплуатационный риск коллектора: +данные просто перестают приходить, и заметить это можно только по молчанию. +Расписание HAE — пожелание, а не гарантия (находка 28), так что молчание +случается штатно. + +`/stats` отвечает на «жив ли поток» без чтения логов: последняя доставка по +каждой автоматизации, счётчики за сутки, тишина в часах, доля доставок с +ошибкой разбора, строки без кода в словаре категориальных значений. + +Готово, когда по одному запросу видно, какая из автоматизаций замолчала и +когда. + +Активное уведомление — отдельная задача, здесь только факт. + diff --git a/docs/backlog/sutki-i-chasovoj-poyas.md b/docs/backlog/sutki-i-chasovoj-poyas.md new file mode 100644 index 0000000..4169c25 --- /dev/null +++ b/docs/backlog/sutki-i-chasovoj-poyas.md @@ -0,0 +1,20 @@ +# [idea] Что считать сутками при смене часового пояса + +**Приоритет:** средний + +«Шаги за день» — базовый запрос трекера и фитнес-игры. Но точка несёт метку с +офсетом исходной зоны, и при перелёте сутки перестают быть однозначными: день +по локальному времени в момент измерения, по текущей зоне телефона или по +фиксированной зоне пользователя — три разных числа. + +Apple эту неоднозначность не решает, а перекладывает: в экспорте у записи есть +и время, и офсет. Мы храним так же — значит выбор всплывает ровно в момент +свёртки. + +Почему идея, а не задача: непонятно, что должно стать наблюдаемо иначе. Нужно +решить, чей это выбор — сервера (одна зона в конфиге), клиента (параметр +запроса) или обоих (умолчание плюс переопределение). + +Вопрос не гипотетический: походы и хайкинг из второго сценария — это как раз +поездки со сменой зоны. + diff --git a/docs/backlog/trenirovki-i-zapisi.md b/docs/backlog/trenirovki-i-zapisi.md new file mode 100644 index 0000000..a5d4649 --- /dev/null +++ b/docs/backlog/trenirovki-i-zapisi.md @@ -0,0 +1,23 @@ +# Тренировки и секции с собственными id + +**Приоритет:** высокий + +Тренировки приезжают с геотреком, состояние разума — с кодами HealthKit. Ни то, +ни другое сейчас не разбирается. Тренировки нужны трекеру (второй сценарий), +состояние разума — агенту-медику. + +Модель отличается от метрик: у этих сущностей есть собственный `id`, они редки, +и по часам их группировать незачем. Тренировка **перезаписывается** целиком — +она приезжает повторно, когда доедет маршрут. + +Шаги: +- миграции `workout` и `record` (секции `stateOfMind`, `ecg`, `symptoms`, + `cycleTracking`, `medications`, `heartRateNotifications` — модель одна); +- заголовок тренировки колонками, маршрут и внутренние ряды — блобом; +- пульс внутри тренировки не смешивать с метрикой `heart_rate`: разные таблицы. + +Готово, когда тренировка отдаётся одним пакетом вместе с маршрутом, а +`stateOfMind` виден записями. + +Связано: `docs/architecture.md` → «Тренировки и прочие секции». + diff --git a/docs/backlog/upravlenie-sekretami.md b/docs/backlog/upravlenie-sekretami.md new file mode 100644 index 0000000..4e28fcc --- /dev/null +++ b/docs/backlog/upravlenie-sekretami.md @@ -0,0 +1,22 @@ +# Управление токенами и секретами + +**Приоритет:** средний + +Сейчас проверка токенов выключена сознательно — доверенная локальная сеть, — и +`config.docker.toml` коммитится без секретов. Для локальной разработки это +правильно, но это же делает выезд наружу опасным: одна забытая настройка +открывает историю здоровья всему интернету. + +Решается перед деплоем, не раньше — так договорились. + +Шаги: +- раздельные токены приёма и чтения, генерация и хранение вне репозитория; +- сервис громко предупреждает на старте, если проверка выключена (уже есть), + и **отказывается стартовать**, если адрес прослушивания публичный, а токенов + нет; +- проверка в гейте, что в коммит не уехал файл с токеном (частично закрыта + `gitleaks`). + +Готово, когда запуск без токенов возможен только на localhost, а на rivendell +оба контура закрыты разными токенами. + diff --git a/docs/backlog/ustarevanie-nizhnego-sloya.md b/docs/backlog/ustarevanie-nizhnego-sloya.md new file mode 100644 index 0000000..36761df --- /dev/null +++ b/docs/backlog/ustarevanie-nizhnego-sloya.md @@ -0,0 +1,22 @@ +# Устаревание нижнего слоя после экспорта + +**Приоритет:** низкий + +Нижний слой растёт примерно на 100 тысяч координат в сутки против ~3 700 у +минутного и ~100 у часового — разница в три порядка (находка 41). Всё давление +по объёму создаёт он один, и ровно там родной экспорт Apple оказывается +настоящим надмножеством. + +Два ограничителя, без которых правило опасно: + +- пометка вешается по **загруженному и проверенному** экспорту, а не по + сделанному: проверка — непрерывность по дням и сходимость сумм с часовым + слоем; +- пометка ≠ удаление. Удаление включается только после того, как восстановление + из экспорта отработает на живых данных хотя бы раз. + +Приоритет низкий: пока история измеряется днями, экономить нечего. Задача +станет актуальной, когда нижний слой перевалит за несколько гигабайт. + +Зависит от импорта экспорта Apple — до него помечать нечем. + diff --git a/docs/plan.md b/docs/plan.md index 7ab3e61..9e7d7a8 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -2,6 +2,10 @@ Живой документ: шаги отмечаем по мере готовности, отложенное копится внизу. +Это **порядок и его обоснование**, а не список работ. Единицы работы живут в +[беклоге](backlog/README.md) — одна задача, один файл, свой приоритет. План +отвечает «почему в таком порядке», беклог — «что брать следующим». + ## Ближайшая цель Приём работает, поток настоящих пакетов копится в сыром архиве. Дальше —