заведён беклог проекта
- 20 задач: 8 высоких, 9 средних, 3 низких; две заведены идеями — сутки при смене часового пояса и пересекающиеся источники одной метрики - план сведён к порядку и его обоснованию, единицы работы переехали в беклог
This commit is contained in:
@@ -56,12 +56,36 @@ Module path — `git.vakhrushev.me/av/healthlog`.
|
|||||||
|
|
||||||
Запуск через [Task](https://taskfile.dev) (`task --list` — полный список):
|
Запуск через [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 build` — статический бинарь linux/amd64
|
||||||
- `task test` / `task lint` — тесты и golangci-lint
|
- `task test` / `task lint` — тесты и golangci-lint
|
||||||
- `task tidy` — `go mod tidy`
|
- `task tidy` — `go mod tidy`
|
||||||
- `task setup` — установка golangci-lint
|
- `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`): форма логов
|
Механизируемое проверяет `task lint` (`.golangci.yml`): форма логов
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Кладбище беклога
|
||||||
|
|
||||||
|
Задачи, покинувшие беклог без реализации. Пишется `backlog.py close`.
|
||||||
|
|
||||||
|
<!-- - ГГГГ-ММ-ДД `slug` — Заголовок. Причина: … Был приоритет: … -->
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Беклог
|
||||||
|
|
||||||
|
Одна задача = один файл `<slug>.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) — Пропажу потока сейчас замечает человек, а не сервис
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Активный алерт «данных нет N часов»
|
||||||
|
|
||||||
|
**Приоритет:** низкий
|
||||||
|
|
||||||
|
Пропажу потока сейчас замечает человек. `/stats` покажет факт, но только если
|
||||||
|
туда заглянуть — а заглядывают ровно тогда, когда уже что-то заподозрили.
|
||||||
|
|
||||||
|
Активное уведомление закрывает разрыв: сервис сам сообщает, что данных нет
|
||||||
|
дольше порога. Канал — тот же, что у остальных моих проектов.
|
||||||
|
|
||||||
|
Порог не единый: быстрый проход идёт каждые 5 минут, но ночью телефон
|
||||||
|
заблокирован и тишина штатна (находка 28). Значит порог считается по времени
|
||||||
|
суток или по последней успешной доставке каждой автоматизации отдельно.
|
||||||
|
|
||||||
|
Приоритет низкий, пока сервис на рабочей машине и я вижу его каждый день.
|
||||||
|
После деплоя на rivendell поднимется.
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Резервное копирование ./data
|
||||||
|
|
||||||
|
**Приоритет:** высокий
|
||||||
|
|
||||||
|
`./data` — это база и 16 МБ сырого архива в единственном экземпляре на рабочей
|
||||||
|
машине. Родной экспорт Apple делается раз в 2–3 месяца, то есть потеря каталога
|
||||||
|
стоит всей истории с момента последнего экспорта.
|
||||||
|
|
||||||
|
Отдельная тонкость: SQLite нельзя копировать `cp` под нагрузкой, а телефон шлёт
|
||||||
|
непрерывно — нужен `VACUUM INTO` или backup API, а не копирование файла.
|
||||||
|
|
||||||
|
Приоритет высокий не по ценности, а по асимметрии: задача на пару часов
|
||||||
|
страхует данные, которые иначе не восстановить ничем.
|
||||||
|
|
||||||
|
Готово, когда есть команда снятия копии, она отрабатывает на живой базе под
|
||||||
|
приёмом, и копия проверяется восстановлением хотя бы раз.
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Деплой на rivendell
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
Сервис живёт в контейнере на рабочей машине, телефон достаёт до него только
|
||||||
|
дома. Вне дома экспорт копится и уезжает пачкой при возвращении — работает, но
|
||||||
|
это не то, ради чего заводился всегда доступный VPS.
|
||||||
|
|
||||||
|
Есть готовый образец: jellybit собирает образ локально и отправляет на сервер
|
||||||
|
через `docker save`/`load`, Caddy впереди терминирует TLS. Здесь то же самое.
|
||||||
|
|
||||||
|
Шаги:
|
||||||
|
- сборка образа локально, доставка на rivendell;
|
||||||
|
- Caddy: поддомен приёма и поддомен чтения (плюс MCP на нём же);
|
||||||
|
- тома под `./data`, конфиг с токенами отдельно, права `0600`.
|
||||||
|
|
||||||
|
Готово, когда телефон шлёт на публичный адрес из любой сети, а агент читает по
|
||||||
|
тому же домену.
|
||||||
|
|
||||||
|
Зависит от задачи про секреты: выезжать наружу с выключенной проверкой токенов
|
||||||
|
нельзя.
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Импорт родного экспорта Apple Health
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
Слой `sample` пуст: настоящих сэмплов HealthKit в потоке нет вовсе — HAE отдаёт
|
||||||
|
посекундную развёртку, а не измерения (находка 34). Полная история и точные
|
||||||
|
сэмплы лежат в zip родного экспорта.
|
||||||
|
|
||||||
|
Это же основание для устаревания нижнего слоя и единственный способ поднять
|
||||||
|
историю глубже недели: дыра старше недели проходами синхронизации не чинится.
|
||||||
|
|
||||||
|
Шаги:
|
||||||
|
- разбор `экспорт.xml` (HealthKit Export Version 14, `<Record>` с
|
||||||
|
`startDate`/`endDate`/`value`/`sourceName`/`device`) в слой `sample`;
|
||||||
|
- маршруты GPX и ЭКГ отдельными CSV — они не в XML;
|
||||||
|
- заливка кусками по годам: файл измеряется сотнями мегабайт.
|
||||||
|
|
||||||
|
Готово, когда история за несколько лет лежит в слое `sample`, а суммы по нему
|
||||||
|
сходятся с часовым слоем HAE на пересечении периодов.
|
||||||
|
|
||||||
|
Есть готовый файл для проверки: `/home/av/MediaEverything/HealthData/apple_health/`.
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# MCP-сервер поверх Read API
|
||||||
|
|
||||||
|
**Приоритет:** высокий
|
||||||
|
|
||||||
|
Агент-медик — первый заказчик проекта. Ему нужна актуальная сводка, а не срез
|
||||||
|
на дату последнего ручного экспорта.
|
||||||
|
|
||||||
|
Транспорт — **Streamable HTTP**, не stdio: сервис живёт на VPS, агент ходит по
|
||||||
|
сети. Отсюда: MCP — эндпоинт того же процесса и того же порта, аутентификация —
|
||||||
|
тот же токен чтения, что у Read API. Отдельного контура доступа не заводим:
|
||||||
|
MCP не даёт ничего, чего не даёт HTTP, и права обязаны совпадать.
|
||||||
|
|
||||||
|
Инструментов три: каталог разрезов, значения за период, значения с разбивкой.
|
||||||
|
Собственной логики в адаптере нет.
|
||||||
|
|
||||||
|
Правило размера ответа здесь не украшение, а необходимость: у сетевого агента
|
||||||
|
нет способа «посмотреть поближе» иначе, чем повторным вызовом.
|
||||||
|
|
||||||
|
Готово, когда агент подключается по URL и отвечает на «как я спал на прошлой
|
||||||
|
неделе» без промежуточного кода.
|
||||||
|
|
||||||
|
Связано: `docs/architecture.md` → «MCP», план шаг 7.
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# OpenAPI-спека и Swagger UI
|
||||||
|
|
||||||
|
**Приоритет:** высокий
|
||||||
|
|
||||||
|
Потребителей три, и один из них — агент, который читает контракт машиной.
|
||||||
|
Пересказывать форму ответа в чате не годится, а самоописание из плана покрывает
|
||||||
|
только **содержимое** метрик; форма конверта, коды ответов и параметры запроса —
|
||||||
|
это OpenAPI.
|
||||||
|
|
||||||
|
Заодно снимает вопрос, чем быть «схеме контракта API» из раздела самоописания:
|
||||||
|
ею и будет OpenAPI-документ, а не собственный формат.
|
||||||
|
|
||||||
|
Шаги:
|
||||||
|
- спека OpenAPI 3.1 на приём, каталог, точки, тренировки, записи, `/stats`;
|
||||||
|
- Swagger UI на отдельном пути, отдаётся самим сервисом (без внешних CDN —
|
||||||
|
он должен работать в локальной сети без интернета);
|
||||||
|
- проверка актуальности спеки в гейте: контракт разъезжается молча.
|
||||||
|
|
||||||
|
Готово, когда по спеке можно сгенерировать клиент, а Swagger UI открывается
|
||||||
|
локально и выполняет запрос к живому сервису.
|
||||||
|
|
||||||
|
Развилка на решение: спека пишется руками как источник истины или выводится из
|
||||||
|
кода. Для маленького API рукописная спека честнее — но это стоит обсудить.
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# [idea] Пересекающиеся источники одной метрики
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
Одну метрику пишут несколько источников: сон — часы и стороннее приложение
|
||||||
|
AutoSleep, шаги — часы и телефон одновременно. Поле `source` при этом не
|
||||||
|
источник, а множество вкладчиков (`Apple Watch Ultra 3|iPhone (Anton)`), и
|
||||||
|
состав меняется от группировки (находка 36).
|
||||||
|
|
||||||
|
По координатному ключу столкновений почти нет — источники пишут в разные метки.
|
||||||
|
Но по **времени** интервалы пересекаются, и сумма по обоим задвоит ночь сна или
|
||||||
|
дневные шаги.
|
||||||
|
|
||||||
|
Почему идея: неясно, чья это ответственность. Варианты — отдавать как есть и
|
||||||
|
предупреждать в каталоге, выбирать источник по приоритету, отдавать разбивку по
|
||||||
|
источникам отдельным разрезом. Первое честнее всего, третье полезнее всего.
|
||||||
|
|
||||||
|
Для агента-медика вопрос практический: «сколько я спал» не должно давать
|
||||||
|
двойной ответ.
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Проверка секций, которых поток ещё не приносил
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
Разбор пишется по тем данным, что видел поток, а он приносил только `metrics`,
|
||||||
|
`workouts` и `stateOfMind`. Не виденны живьём: `symptoms`, `ecg`,
|
||||||
|
`heartRateNotifications`, `cycleTracking`, `medications`, а также вес — а вес
|
||||||
|
агенту-медику нужен наверняка.
|
||||||
|
|
||||||
|
Пользователь настраивает оставшиеся метрики на телефоне, так что данные
|
||||||
|
появятся сами. Задача — не пропустить момент: убедиться, что новые секции
|
||||||
|
разбираются, а не молча падают в `parse_status`.
|
||||||
|
|
||||||
|
Отдельный вопрос, на который ответят эти же данные: есть ли эти секции в родном
|
||||||
|
экспорте Apple. Если нет — экспорт им не источник истины, и устаревание
|
||||||
|
нижнего слоя к ним неприменимо, держим всегда.
|
||||||
|
|
||||||
|
Готово, когда каждая новая секция либо разобрана, либо явно описана в
|
||||||
|
`docs/local-research.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.
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Read API: точки, выбор слоя, свёртка по сетке
|
||||||
|
|
||||||
|
**Приоритет:** высокий
|
||||||
|
|
||||||
|
Сейчас данные достаются только `sqlite3` на хосте. Все три сценария —
|
||||||
|
агент-медик, трекер тренировок, фитнес-игра — упираются в отсутствие чтения.
|
||||||
|
|
||||||
|
Формы запроса ровно две, и это один запрос с необязательным параметром:
|
||||||
|
`?from&to` — все значения за период (вес, лекарства, симптомы), `?from&to&bucket`
|
||||||
|
— с разбивкой (шаги, энергия).
|
||||||
|
|
||||||
|
Решение по размеру ответа (вариант «б»): разбивка не задана и ответ не влезает —
|
||||||
|
сервер сам берёт сетку погрубее и **называет её в ответе**; разбивка задана явно
|
||||||
|
и не влезает — ошибка со списком доступных сеток, а не тихая подмена. Различие
|
||||||
|
существенно: иначе агент, попросивший минутную сетку, получит суточные суммы.
|
||||||
|
|
||||||
|
Готово, когда «шаги за неделю по дням» и «вес за год» отвечаются одним запросом
|
||||||
|
каждый, а в ответе всегда видно `layer`, `bucket` и `aggregation`.
|
||||||
|
|
||||||
|
Связано: `docs/architecture.md` → «Read API», план шаг 5.
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Пересборка хранилища из сырого архива
|
||||||
|
|
||||||
|
**Приоритет:** высокий
|
||||||
|
|
||||||
|
Разбор пишется по реальным данным и будет ошибаться — это норма, а не риск.
|
||||||
|
Риск в другом: сырой архив живёт 14 дней, и окно на исправление ошибки равно
|
||||||
|
этому сроку. Без `reindex` ошибка разбора становится потерей данных.
|
||||||
|
|
||||||
|
Пересчёт по всей истории сразу ещё и **точнее** приёма: вывод слоя и род
|
||||||
|
агрегации на полном ряду доставок надёжнее, чем на одной.
|
||||||
|
|
||||||
|
Готово, когда `healthlog reindex` пересобирает хранилище с нуля из `data/raw`
|
||||||
|
и результат совпадает с накопленным приёмом.
|
||||||
|
|
||||||
|
Связано: план шаг 3, `docs/architecture.md` → «Сырой архив».
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Ретеншен сырого архива
|
||||||
|
|
||||||
|
**Приоритет:** низкий
|
||||||
|
|
||||||
|
Срок жизни сырого архива объявлен (14 дней, `storage.raw_retention`), но
|
||||||
|
удаления нет: архив растёт бесконечно. Пока это 16 МБ и проблемой не является.
|
||||||
|
|
||||||
|
Включать **после** того, как разбор устоится и `reindex` докажет, что
|
||||||
|
хранилище действительно пересобирается: иначе страховка исчезнет раньше, чем
|
||||||
|
перестанет быть нужна.
|
||||||
|
|
||||||
|
Готово, когда старые тела удаляются по расписанию, а `/stats` показывает
|
||||||
|
глубину архива в днях.
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Измеренный род агрегации и каталог разрезов
|
||||||
|
|
||||||
|
**Приоритет:** высокий
|
||||||
|
|
||||||
|
Решено (вариант «б» груминга): свёртка живёт в ответе, но род метрики
|
||||||
|
**измеряется**, а не размечается руками. Форма точки рода не выдаёт —
|
||||||
|
`Avg`/`Min`/`Max` есть только у `heart_rate`, всё остальное приходит в `qty`
|
||||||
|
(находка 40). Единицы дают процентов девяносто и ломаются на краях.
|
||||||
|
|
||||||
|
Метод: одна метрика лежит в минутном и часовом разрезе одновременно. Часовое
|
||||||
|
значение сходится с суммой минутных — накопительная; со средним — мгновенная;
|
||||||
|
данных не хватило — `unknown`, и свёртка по такой метрике не предлагается вовсе.
|
||||||
|
|
||||||
|
Жёсткое правило: накопительные метрики никогда не сворачиваются из нижнего слоя
|
||||||
|
HAE. Он не сэмплы, а посекундная развёртка (находка 34), сумма по нему завышена.
|
||||||
|
|
||||||
|
Готово, когда каталог отдаёт по каждой метрике единицы, род и список слоёв с
|
||||||
|
диапазонами, а род проставлен измерением на живой истории.
|
||||||
|
|
||||||
|
Связано: `docs/architecture.md` → «Слои гранулярности», план шаг 4.
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Выведенные из данных схемы содержимого
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
Метрик у Apple больше сотни, формы точек разные, и рукописный каталог описывал
|
||||||
|
бы документацию HAE, а не то, что он реально прислал. Схема содержимого
|
||||||
|
**выводится из данных** тем же проходом разбора: незнакомая метрика описывает
|
||||||
|
себя сама, без релиза.
|
||||||
|
|
||||||
|
Схема отдаётся вместе со статистикой — для потребителя она важнее формального
|
||||||
|
типа: сколько точек, первая и последняя метка, единицы, доля присутствия поля.
|
||||||
|
|
||||||
|
Вывод ограничивается по глубине вложенности, иначе схема тренировки с маршрутом
|
||||||
|
разрастётся до размеров самих данных.
|
||||||
|
|
||||||
|
Готово, когда клиент по `/api/v1/metrics/{name}/schema` видит поля, их типы и
|
||||||
|
присутствие, не выкачивая выборку.
|
||||||
|
|
||||||
|
Связано: `docs/architecture.md` → «Самоописание». Форму конверта API описывает
|
||||||
|
не эта задача, а OpenAPI.
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Словарь категориальных значений → коды HealthKit
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
HAE отдаёт перечислимые значения строками локали телефона: «БДГ», «Сидячий
|
||||||
|
образ жизни», «В помещении Ходьба». Родной экспорт Apple при этом говорит
|
||||||
|
кодами (`HKCategoryValueSleepAnalysisAsleepREM`) — источники несопоставимы
|
||||||
|
(находка 37).
|
||||||
|
|
||||||
|
Три следствия, и третье решающее: клиент угадывает словарь; смена языка
|
||||||
|
телефона молча расколет историю; сверить покрытие экспортом нечем — а на этой
|
||||||
|
сверке стоит устаревание нижнего слоя.
|
||||||
|
|
||||||
|
Решение (вариант «б»): строка хранится **дословно**, рядом кладётся выведенный
|
||||||
|
код. Словарь ключуется парой `(локаль, строка)`, локаль берётся из
|
||||||
|
`Accept-Language`. Незнакомая строка → пустой код, а не догадка.
|
||||||
|
|
||||||
|
Готово, когда фазы сна из потока и из экспорта Apple сравниваются напрямую, а
|
||||||
|
`/stats` показывает строки, для которых кода ещё нет.
|
||||||
|
|
||||||
|
`stateOfMind` в словаре не нуждается — он и так шлёт коды HealthKit.
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Наблюдаемость: /stats
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
Тихо сломавшаяся автоматизация — главный эксплуатационный риск коллектора:
|
||||||
|
данные просто перестают приходить, и заметить это можно только по молчанию.
|
||||||
|
Расписание HAE — пожелание, а не гарантия (находка 28), так что молчание
|
||||||
|
случается штатно.
|
||||||
|
|
||||||
|
`/stats` отвечает на «жив ли поток» без чтения логов: последняя доставка по
|
||||||
|
каждой автоматизации, счётчики за сутки, тишина в часах, доля доставок с
|
||||||
|
ошибкой разбора, строки без кода в словаре категориальных значений.
|
||||||
|
|
||||||
|
Готово, когда по одному запросу видно, какая из автоматизаций замолчала и
|
||||||
|
когда.
|
||||||
|
|
||||||
|
Активное уведомление — отдельная задача, здесь только факт.
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# [idea] Что считать сутками при смене часового пояса
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
«Шаги за день» — базовый запрос трекера и фитнес-игры. Но точка несёт метку с
|
||||||
|
офсетом исходной зоны, и при перелёте сутки перестают быть однозначными: день
|
||||||
|
по локальному времени в момент измерения, по текущей зоне телефона или по
|
||||||
|
фиксированной зоне пользователя — три разных числа.
|
||||||
|
|
||||||
|
Apple эту неоднозначность не решает, а перекладывает: в экспорте у записи есть
|
||||||
|
и время, и офсет. Мы храним так же — значит выбор всплывает ровно в момент
|
||||||
|
свёртки.
|
||||||
|
|
||||||
|
Почему идея, а не задача: непонятно, что должно стать наблюдаемо иначе. Нужно
|
||||||
|
решить, чей это выбор — сервера (одна зона в конфиге), клиента (параметр
|
||||||
|
запроса) или обоих (умолчание плюс переопределение).
|
||||||
|
|
||||||
|
Вопрос не гипотетический: походы и хайкинг из второго сценария — это как раз
|
||||||
|
поездки со сменой зоны.
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Тренировки и секции с собственными id
|
||||||
|
|
||||||
|
**Приоритет:** высокий
|
||||||
|
|
||||||
|
Тренировки приезжают с геотреком, состояние разума — с кодами HealthKit. Ни то,
|
||||||
|
ни другое сейчас не разбирается. Тренировки нужны трекеру (второй сценарий),
|
||||||
|
состояние разума — агенту-медику.
|
||||||
|
|
||||||
|
Модель отличается от метрик: у этих сущностей есть собственный `id`, они редки,
|
||||||
|
и по часам их группировать незачем. Тренировка **перезаписывается** целиком —
|
||||||
|
она приезжает повторно, когда доедет маршрут.
|
||||||
|
|
||||||
|
Шаги:
|
||||||
|
- миграции `workout` и `record` (секции `stateOfMind`, `ecg`, `symptoms`,
|
||||||
|
`cycleTracking`, `medications`, `heartRateNotifications` — модель одна);
|
||||||
|
- заголовок тренировки колонками, маршрут и внутренние ряды — блобом;
|
||||||
|
- пульс внутри тренировки не смешивать с метрикой `heart_rate`: разные таблицы.
|
||||||
|
|
||||||
|
Готово, когда тренировка отдаётся одним пакетом вместе с маршрутом, а
|
||||||
|
`stateOfMind` виден записями.
|
||||||
|
|
||||||
|
Связано: `docs/architecture.md` → «Тренировки и прочие секции».
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Управление токенами и секретами
|
||||||
|
|
||||||
|
**Приоритет:** средний
|
||||||
|
|
||||||
|
Сейчас проверка токенов выключена сознательно — доверенная локальная сеть, — и
|
||||||
|
`config.docker.toml` коммитится без секретов. Для локальной разработки это
|
||||||
|
правильно, но это же делает выезд наружу опасным: одна забытая настройка
|
||||||
|
открывает историю здоровья всему интернету.
|
||||||
|
|
||||||
|
Решается перед деплоем, не раньше — так договорились.
|
||||||
|
|
||||||
|
Шаги:
|
||||||
|
- раздельные токены приёма и чтения, генерация и хранение вне репозитория;
|
||||||
|
- сервис громко предупреждает на старте, если проверка выключена (уже есть),
|
||||||
|
и **отказывается стартовать**, если адрес прослушивания публичный, а токенов
|
||||||
|
нет;
|
||||||
|
- проверка в гейте, что в коммит не уехал файл с токеном (частично закрыта
|
||||||
|
`gitleaks`).
|
||||||
|
|
||||||
|
Готово, когда запуск без токенов возможен только на localhost, а на rivendell
|
||||||
|
оба контура закрыты разными токенами.
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Устаревание нижнего слоя после экспорта
|
||||||
|
|
||||||
|
**Приоритет:** низкий
|
||||||
|
|
||||||
|
Нижний слой растёт примерно на 100 тысяч координат в сутки против ~3 700 у
|
||||||
|
минутного и ~100 у часового — разница в три порядка (находка 41). Всё давление
|
||||||
|
по объёму создаёт он один, и ровно там родной экспорт Apple оказывается
|
||||||
|
настоящим надмножеством.
|
||||||
|
|
||||||
|
Два ограничителя, без которых правило опасно:
|
||||||
|
|
||||||
|
- пометка вешается по **загруженному и проверенному** экспорту, а не по
|
||||||
|
сделанному: проверка — непрерывность по дням и сходимость сумм с часовым
|
||||||
|
слоем;
|
||||||
|
- пометка ≠ удаление. Удаление включается только после того, как восстановление
|
||||||
|
из экспорта отработает на живых данных хотя бы раз.
|
||||||
|
|
||||||
|
Приоритет низкий: пока история измеряется днями, экономить нечего. Задача
|
||||||
|
станет актуальной, когда нижний слой перевалит за несколько гигабайт.
|
||||||
|
|
||||||
|
Зависит от импорта экспорта Apple — до него помечать нечем.
|
||||||
|
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
Живой документ: шаги отмечаем по мере готовности, отложенное копится внизу.
|
Живой документ: шаги отмечаем по мере готовности, отложенное копится внизу.
|
||||||
|
|
||||||
|
Это **порядок и его обоснование**, а не список работ. Единицы работы живут в
|
||||||
|
[беклоге](backlog/README.md) — одна задача, один файл, свой приоритет. План
|
||||||
|
отвечает «почему в таком порядке», беклог — «что брать следующим».
|
||||||
|
|
||||||
## Ближайшая цель
|
## Ближайшая цель
|
||||||
|
|
||||||
Приём работает, поток настоящих пакетов копится в сыром архиве. Дальше —
|
Приём работает, поток настоящих пакетов копится в сыром архиве. Дальше —
|
||||||
|
|||||||
Reference in New Issue
Block a user