tasks: закрыт разбор и хранилище, начат спринт по чтению данных клиентами
- цель parsing-and-storage закрыта по своему критерию; незакрываемый остаток (новые формы от источника, ручные секции задним числом) переехал в тему parsing-completeness - цель mcp поглощена целью read-api, переименованной в «Чтение данных клиентами»: адаптер — последний шаг того же направления, а не своё - read-api-points разложена на конверт с точками, свёртку по сетке и тренировки с записями; спринт 2026-08-04 набран пятью задачами
This commit is contained in:
@@ -16,10 +16,8 @@
|
|||||||
- [Сущность с id, но неразобранной меткой](items/entity-without-parsed-label.md) — Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым
|
- [Сущность с id, но неразобранной меткой](items/entity-without-parsed-label.md) — Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым
|
||||||
- [Идентичность тренировок при импорте родного экспорта](items/workout-identity-on-import.md) — В export.xml у тренировки нет id — импорт снапшота задвоит тренировки, приехавшие от HAE
|
- [Идентичность тренировок при импорте родного экспорта](items/workout-identity-on-import.md) — В export.xml у тренировки нет id — импорт снапшота задвоит тренировки, приехавшие от HAE
|
||||||
- [Импорт родного экспорта Apple Health](items/apple-export-import.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут
|
- [Импорт родного экспорта Apple Health](items/apple-export-import.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут
|
||||||
- [MCP-сервер поверх Read API](items/mcp-server.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
|
|
||||||
- [[idea] Месячный проход по ручным секциям](items/monthly-manual-sections-pass.md) — Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит
|
- [[idea] Месячный проход по ручным секциям](items/monthly-manual-sections-pass.md) — Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит
|
||||||
- [[idea] NDJSON-поток для больших выборок Read API](items/ndjson-stream.md) — Выборка нижнего слоя за месяц не влезает в один JSON-ответ — либо поток, либо пагинация
|
- [[idea] NDJSON-поток для больших выборок Read API](items/ndjson-stream.md) — Выборка нижнего слоя за месяц не влезает в один JSON-ответ — либо поток, либо пагинация
|
||||||
- [OpenAPI-спека и Swagger UI](items/openapi-swagger.md) — Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате
|
|
||||||
- [Data-миграции не отбирают строки по обрезаемым спискам](items/data-migration-row-selection.md) — Миграция 00007 отбирает по uncovered_sections, который обрезается на 32 — следующая покрытая секция унаследует слепую зону
|
- [Data-миграции не отбирают строки по обрезаемым спискам](items/data-migration-row-selection.md) — Миграция 00007 отбирает по uncovered_sections, который обрезается на 32 — следующая покрытая секция унаследует слепую зону
|
||||||
- [[idea] Отказ от heartbeatSeries](items/drop-heartbeat-series.md) — 93% объёма HRV ради данных, которых нет ни в одном планируемом запросе
|
- [[idea] Отказ от heartbeatSeries](items/drop-heartbeat-series.md) — 93% объёма HRV ради данных, которых нет ни в одном планируемом запросе
|
||||||
- [Пересборка держит весь журнал в памяти](items/rebuild-memory-footprint.md) — Учёт доставок и список путей архива материализуются целиком: расход растёт вместе с журналом, а у журнала конца нет
|
- [Пересборка держит весь журнал в памяти](items/rebuild-memory-footprint.md) — Учёт доставок и список путей архива материализуются целиком: расход растёт вместе с журналом, а у журнала конца нет
|
||||||
@@ -29,7 +27,6 @@
|
|||||||
- [Предел на размер и число заголовков доставки](items/delivery-header-limits.md) — MaxHeaderBytes не задан, в базу заголовки пишутся целиком: дефект спит до деплоя, а просыпается вместе с ним
|
- [Предел на размер и число заголовков доставки](items/delivery-header-limits.md) — MaxHeaderBytes не задан, в базу заголовки пишутся целиком: дефект спит до деплоя, а просыпается вместе с ним
|
||||||
- [Пределы на размер сущности и потоковый расчёт формы](items/entity-size-limits.md) — Тело 40 МиБ даёт 768 МиБ пика кучи, 63 МиБ держат блокировку 5.019 с — предела на одну сущность нет вовсе
|
- [Пределы на размер сущности и потоковый расчёт формы](items/entity-size-limits.md) — Тело 40 МиБ даёт 768 МиБ пика кучи, 63 МиБ держат блокировку 5.019 с — предела на одну сущность нет вовсе
|
||||||
- [[idea] Разворачивание маршрутов тренировок в отдельную таблицу](items/workout-routes-table.md) — Маршрут лежит блобом внутри тренировки — понадобится, только если появится клиент, которому мало отдачи одним пакетом
|
- [[idea] Разворачивание маршрутов тренировок в отдельную таблицу](items/workout-routes-table.md) — Маршрут лежит блобом внутри тренировки — понадобится, только если появится клиент, которому мало отдачи одним пакетом
|
||||||
- [Read API: точки, выбор слоя, свёртка по сетке](items/read-api-points.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может
|
|
||||||
- [Выведенные из данных схемы содержимого](items/derived-content-schemas.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке
|
- [Выведенные из данных схемы содержимого](items/derived-content-schemas.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке
|
||||||
- [[idea] Что считать сутками при смене часового пояса](items/day-boundary-timezone.md) — Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено
|
- [[idea] Что считать сутками при смене часового пояса](items/day-boundary-timezone.md) — Шаги за день — базовый запрос трекера и игры, но чей это день при перелёте, не решено
|
||||||
- [Сверка живой витрины с пересборкой](items/rebuild-comparison-check.md) — reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит
|
- [Сверка живой витрины с пересборкой](items/rebuild-comparison-check.md) — reindex печатает оба отпечатка, но сравнивать их некому — расхождение с журналом молчит
|
||||||
|
|||||||
+14
-5
@@ -16,6 +16,13 @@
|
|||||||
слоя, модель идентичности и формы точки проверены на живом потоке
|
слоя, модель идентичности и формы точки проверены на живом потоке
|
||||||
([research/apple-health.md](../research/apple-health.md)).
|
([research/apple-health.md](../research/apple-health.md)).
|
||||||
|
|
||||||
|
**Разбор и хранилище закрыты 2026-08-04.** Ни одна секция живого потока не
|
||||||
|
числится неразобранной, категориальные значения несут стабильный код рядом с
|
||||||
|
переведённой строкой, а первая встреча незнакомой секции стала наблюдаемым
|
||||||
|
событием. То, что заканчиваться не умеет по природе — новые формы от источника
|
||||||
|
и ручные секции задним числом, — переехало в тему
|
||||||
|
[«Полнота разбора потока»](items/parsing-completeness.md).
|
||||||
|
|
||||||
Эти звенья целями не заведены: закрытая цель записи не оставляет, ей хватает
|
Эти звенья целями не заведены: закрытая цель записи не оставляет, ей хватает
|
||||||
коммита и спеки.
|
коммита и спеки.
|
||||||
|
|
||||||
@@ -26,14 +33,15 @@
|
|||||||
нижний слой значит завысить втрое. Это звено уже закрыто.
|
нижний слой значит завысить втрое. Это звено уже закрыто.
|
||||||
- **`healthlog import` — перед устареванием нижнего слоя.** Пока импорт
|
- **`healthlog import` — перед устареванием нижнего слоя.** Пока импорт
|
||||||
экспорта не написан, помечать что-либо устаревшим не на основании чего.
|
экспорта не написан, помечать что-либо устаревшим не на основании чего.
|
||||||
- **Read API — перед MCP.** Адаптер собственной логики не несёт, он переводит
|
- **MCP — внутри чтения, а не отдельной целью.** Прежде было две цели, и
|
||||||
вызовы в те же обработчики; переводить пока нечего.
|
разделяла их очередь: адаптер собственной логики не несёт, он переводит
|
||||||
|
вызовы в те же обработчики, и переводить было нечего. Очередь осталась —
|
||||||
|
порядком задач внутри цели, — а отдельная цель под адаптер описывала не
|
||||||
|
направление, а последний шаг того же направления.
|
||||||
|
|
||||||
## порядок
|
## порядок
|
||||||
- [[goal] Разбор и хранилище](items/parsing-and-storage.md) — Секции живого потока разобраны все, кроме словаря категориальных значений и пяти невиденных
|
- [[goal] Чтение данных клиентами](items/read-api.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей, включая агента-медика, ничего прочитать не может
|
||||||
- [[goal] Read API](items/read-api.md) — Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может
|
|
||||||
- [[goal] Самоописание](items/self-description.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке
|
- [[goal] Самоописание](items/self-description.md) — Метрик больше сотни и формы точек разные — клиент вынужден угадывать структуру по выборке
|
||||||
- [[goal] MCP](items/mcp.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
|
|
||||||
- [[goal] Импорт родного экспорта Apple](items/native-export-import.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут
|
- [[goal] Импорт родного экспорта Apple](items/native-export-import.md) — Слой sample пуст: полная история и точные сэмплы лежат в zip-архиве и никуда не едут
|
||||||
- [[goal] Устаревание нижнего слоя](items/lower-layer-cleanup.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен
|
- [[goal] Устаревание нижнего слоя](items/lower-layer-cleanup.md) — Нижний слой растёт на ~100 тысяч координат в сутки, а после экспорта Apple он избыточен
|
||||||
- [[goal] Наблюдаемость](items/observability.md) — Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах
|
- [[goal] Наблюдаемость](items/observability.md) — Тихо сломавшаяся автоматизация — главный эксплуатационный риск, а сейчас факт виден только в логах
|
||||||
@@ -43,3 +51,4 @@
|
|||||||
- [[goal] Прочность слияния и идентичности](items/merge-robustness.md) — Правила слияния держатся на замерах, и каждый новый замер находит зависимость от порядка на проводе
|
- [[goal] Прочность слияния и идентичности](items/merge-robustness.md) — Правила слияния держатся на замерах, и каждый новый замер находит зависимость от порядка на проводе
|
||||||
- [[goal] Журнал и пересборка](items/journal-and-rebuild.md) — Расхождение витрины с журналом сейчас молчит: reindex печатает оба отпечатка, а сравнивать их некому
|
- [[goal] Журнал и пересборка](items/journal-and-rebuild.md) — Расхождение витрины с журналом сейчас молчит: reindex печатает оба отпечатка, а сравнивать их некому
|
||||||
- [[goal] Пределы и поведение под объёмом](items/limits-and-load.md) — Предела на одну сущность нет вовсе: 63 МиБ держат блокировку 5.019 с, соседние доставки уходят в failed
|
- [[goal] Пределы и поведение под объёмом](items/limits-and-load.md) — Предела на одну сущность нет вовсе: 63 МиБ держат блокировку 5.019 с, соседние доставки уходят в failed
|
||||||
|
- [[goal] Полнота разбора потока](items/parsing-completeness.md) — Пять секций HAE поток ещё не приносил, а ручные секции заводятся задним числом — разбор полон ровно до следующей новой формы
|
||||||
|
|||||||
@@ -8,3 +8,5 @@
|
|||||||
- 2026-08-01 `bekap-dannyh` — Резервное копирование ./data. Причина: бекап обеспечивает готовый механизм на сервере пет-проектов — своего заводить не нужно, задача снимается деплоем. Была секция: высокий.
|
- 2026-08-01 `bekap-dannyh` — Резервное копирование ./data. Причина: бекап обеспечивает готовый механизм на сервере пет-проектов — своего заводить не нужно, задача снимается деплоем. Была секция: высокий.
|
||||||
- 2026-08-01 `identichnost-epizodnyh-metrik` — Идентичность эпизодных метрик. Причина: решён измерением и prior art: ключ эпизода — метрика+слой+start+end (находка 47), вариант А; вернулся в scope razbor-metrik-v-obekty. Была секция: блокеры.
|
- 2026-08-01 `identichnost-epizodnyh-metrik` — Идентичность эпизодных метрик. Причина: решён измерением и prior art: ключ эпизода — метрика+слой+start+end (находка 47), вариант А; вернулся в scope razbor-metrik-v-obekty. Была секция: блокеры.
|
||||||
- 2026-08-01 `edinicy-metriki-v-razreze` — Единицы метрики: часть координаты или свойство объекта. Причина: измерено: на 99 доставках единицы не менялись ни у одной из 30 метрик (находка 49 → 48); реализованное правило «сохранённое побеждает + WARN + счётчик» делает событие наблюдаемым. Была секция: блокеры.
|
- 2026-08-01 `edinicy-metriki-v-razreze` — Единицы метрики: часть координаты или свойство объекта. Причина: измерено: на 99 доставках единицы не менялись ни у одной из 30 метрик (находка 49 → 48); реализованное правило «сохранённое побеждает + WARN + счётчик» делает событие наблюдаемым. Была секция: блокеры.
|
||||||
|
- 2026-08-04 `mcp` — [goal] MCP. Причина: поглощена целью read-api («Чтение данных клиентами»): MCP — не направление, а последний шаг того же направления; адаптер переводит вызовы в те же обработчики и собственной логики не несёт. Очередь «Read API перед MCP» стала порядком задач внутри цели. Задача mcp-server жива и перевешена на read-api. Была секция: порядок.
|
||||||
|
- 2026-08-04 `read-api-points` — Read API: точки, выбор слоя, свёртка по сетке. Причина: разложена на read-api-envelope-and-points (конверт, точки за период, форма провода, условный запрос), read-api-bucketing (свёртка по сетке, предел размера ответа, порог неполного ведра) и read-api-workouts-and-records (тренировки и записи наружу). Одним заходом не мерджилась: десяток критериев приёмки и три развилки в одном файле. Была секция: ядро.
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
# Спринт
|
# Спринт
|
||||||
|
|
||||||
**Цель:** [[goal] Разбор и хранилище](items/parsing-and-storage.md) · **Начат:** 2026-08-03 · **Спринт:** `2026-08-03`
|
- **Цель:** [[goal] Чтение данных клиентами](items/read-api.md)
|
||||||
|
- **Начат:** 2026-08-04
|
||||||
|
- **Спринт:** `2026-08-04`
|
||||||
|
|
||||||
Урожай спринта поднимается `tasks.py list --tag sprint:2026-08-03` — это первая порция переоценки на сессии.
|
Урожай спринта поднимается `tasks.py list --tag sprint:2026-08-04` — это первая порция переоценки на сессии.
|
||||||
|
|
||||||
## Набор
|
## Набор
|
||||||
|
- [Конверт ответа и точки за период](items/read-api-envelope-and-points.md) — Точки лежат в витрине и наружу не отдаются: ни один потребитель не может спросить «вес за год» иначе как sqlite3 на хосте
|
||||||
|
- [Свёртка по сетке и предел размера ответа](items/read-api-bucketing.md) — Враждебный запрос к каталогу стоил 693 мс и +153 МиБ кучи, а у точек множители те же и потолка нет ни у одного
|
||||||
|
- [Тренировки и записи наружу](items/read-api-workouts-and-records.md) — Тренировки с маршрутами и stateOfMind разобраны и лежат в витрине, а эндпоинтов нет — второй сценарий паспорта не закрыт
|
||||||
|
- [OpenAPI-спека и Swagger UI](items/openapi-swagger.md) — Потребителей три и один из них агент — контракт должен читаться машиной, а не пересказываться в чате
|
||||||
|
- [MCP-сервер поверх Read API](items/mcp-server.md) — Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
- **Секция:** ядро
|
- **Секция:** ядро
|
||||||
- **Зачем:** Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым
|
- **Зачем:** Тренировка с меткой в неизвестном формате пропадает целиком, а после включения ретеншена окно становится необратимым
|
||||||
- **Теги:** goal:parsing-and-storage
|
- **Теги:** goal:parsing-completeness
|
||||||
|
|
||||||
**Решение принято владельцем 2026-08-03: вариант (1) — хранить с NULL-меткой.**
|
**Решение принято владельцем 2026-08-03: вариант (1) — хранить с NULL-меткой.**
|
||||||
`start_utc`/`ts_utc` становятся NULLABLE, содержимое (включая маршрут) хранится,
|
`start_utc`/`ts_utc` становятся NULLABLE, содержимое (включая маршрут) хранится,
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
отвергнут при постановке: подстановка метки доставки — выдуманное измерение в
|
отвергнут при постановке: подстановка метки доставки — выдуманное измерение в
|
||||||
колонке, по которой идёт выборка.
|
колонке, по которой идёт выборка.
|
||||||
|
|
||||||
**Берётся после [Read API по точкам и сущностям](read-api-points.md).** Правило
|
**Берётся после [тренировок и записей наружу](read-api-workouts-and-records.md).** Правило
|
||||||
чтения — что выборка «за период» делает со строками без метки — обязано
|
чтения — что выборка «за период» делает со строками без метки — обязано
|
||||||
проектироваться вместе с читателем, иначе такие строки молча исчезнут из любого
|
проектироваться вместе с читателем, иначе такие строки молча исчезнут из любого
|
||||||
ответа. Порядок тот же, что у [journal-order-on-ingest](journal-order-on-ingest.md)
|
ответа. Порядок тот же, что у [journal-order-on-ingest](journal-order-on-ingest.md)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
- **Секция:** ядро
|
- **Секция:** ядро
|
||||||
- **Зачем:** Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
|
- **Зачем:** Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
|
||||||
- **Теги:** goal:mcp
|
- **Теги:** goal:read-api
|
||||||
|
|
||||||
Агент-медик — первый заказчик проекта. Ему нужна актуальная сводка, а не срез
|
Агент-медик — первый заказчик проекта. Ему нужна актуальная сводка, а не срез
|
||||||
на дату последнего ручного экспорта.
|
на дату последнего ручного экспорта.
|
||||||
@@ -21,4 +21,25 @@ MCP не даёт ничего, чего не даёт HTTP, и права об
|
|||||||
Готово, когда агент подключается по URL и отвечает на «как я спал на прошлой
|
Готово, когда агент подключается по URL и отвечает на «как я спал на прошлой
|
||||||
неделе» без промежуточного кода.
|
неделе» без промежуточного кода.
|
||||||
|
|
||||||
Связано: `docs/architecture.md` → «MCP», план → шаг «MCP».
|
## Критерии приёмки
|
||||||
|
|
||||||
|
- живой агент подключается по URL и отвечает на «как я спал на прошлой неделе»
|
||||||
|
без промежуточного кода — оракул: подключение реального MCP-клиента к
|
||||||
|
поднятому сервису
|
||||||
|
- вызов инструмента и соответствующий HTTP-запрос дают одни и те же данные —
|
||||||
|
оракул: тест, сравнивающий выход инструмента с ответом маршрута на тех же
|
||||||
|
параметрах
|
||||||
|
- запрос без токена чтения отклоняется обоими транспортами одинаково — оракул:
|
||||||
|
тест на паре «MCP без токена / HTTP без токена»
|
||||||
|
- правило размера ответа действует и в MCP: слишком широкий запрос получает
|
||||||
|
названную сетку или ошибку со списком, а не обрезанный ответ — оракул: тест на
|
||||||
|
запросе за пределом
|
||||||
|
|
||||||
|
## Рамки
|
||||||
|
|
||||||
|
Схема не трогается, данные только читаются, сервис перезапускается. Собственной
|
||||||
|
логики адаптер не несёт — новое поведение здесь признак того, что оно должно
|
||||||
|
было появиться в маршруте чтения. Берётся последней в цели: переводить нечего,
|
||||||
|
пока обработчиков нет.
|
||||||
|
|
||||||
|
Связано: `docs/architecture.md` → «MCP».
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# [goal] MCP
|
|
||||||
|
|
||||||
- **Секция:** порядок
|
|
||||||
- **Зачем:** Агент-медик — первый заказчик проекта, а подключить его сейчас нечем
|
|
||||||
- **Теги:** decomposed
|
|
||||||
|
|
||||||
Агент-медик — первый заказчик проекта — подключается к хранилищу.
|
|
||||||
|
|
||||||
Выведена из шага 7 плана. Идёт после Read API намеренно: адаптер собственной
|
|
||||||
логики не несёт, он переводит вызовы в те же обработчики, и переводить пока
|
|
||||||
нечего.
|
|
||||||
|
|
||||||
Завершена, когда агент читает данные через MCP тем же токеном чтения.
|
|
||||||
|
|
||||||
## Завершение
|
|
||||||
|
|
||||||
Агент читает данные через MCP тем же токеном чтения, и собственной логики
|
|
||||||
адаптер не несёт.
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
- **Секция:** ядро
|
- **Секция:** ядро
|
||||||
- **Зачем:** Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит
|
- **Зачем:** Симптомы и лекарства заводятся задним числом на недели — недельного глубокого прохода им не хватит
|
||||||
- **Теги:** goal:parsing-and-storage
|
- **Теги:** goal:parsing-completeness
|
||||||
|
|
||||||
Окно досчёта не единое, и это измеренное различие, а не предположение.
|
Окно досчёта не единое, и это измеренное различие, а не предположение.
|
||||||
Количественные метрики (пульс, шаги, энергия) человек руками не правит — они
|
Количественные метрики (пульс, шаги, энергия) человек руками не правит — они
|
||||||
|
|||||||
@@ -19,4 +19,4 @@ Read API отдаёт ответ одним JSON. Для выборок нижн
|
|||||||
последовательно или с возвратами.
|
последовательно или с возвратами.
|
||||||
|
|
||||||
Связано: `docs/architecture.md` → «Свёртка и размер ответа», задача
|
Связано: `docs/architecture.md` → «Свёртка и размер ответа», задача
|
||||||
`read-api-points`.
|
`read-api-bucketing` (правило размера ответа проектируется там).
|
||||||
|
|||||||
@@ -18,8 +18,30 @@
|
|||||||
он должен работать в локальной сети без интернета);
|
он должен работать в локальной сети без интернета);
|
||||||
- проверка актуальности спеки в гейте: контракт разъезжается молча.
|
- проверка актуальности спеки в гейте: контракт разъезжается молча.
|
||||||
|
|
||||||
|
**Решено владельцем 2026-08-04: спека пишется руками и она источник истины.**
|
||||||
|
Для API из шести ручек это честнее вывода из кода: контракт проектируется, а не
|
||||||
|
фотографируется с того, что вышло, — опечатка в имени поля иначе становится
|
||||||
|
частью спеки. Совпадает с тем, как в проекте уже устроен OpenSpec: спека
|
||||||
|
первична к коду. Плата названа: спека расходится с кодом молча, и именно поэтому
|
||||||
|
проверка её актуальности идёт в гейт третьим шагом, а не остаётся регламентом.
|
||||||
|
|
||||||
Готово, когда по спеке можно сгенерировать клиент, а Swagger UI открывается
|
Готово, когда по спеке можно сгенерировать клиент, а Swagger UI открывается
|
||||||
локально и выполняет запрос к живому сервису.
|
локально и выполняет запрос к живому сервису.
|
||||||
|
|
||||||
Развилка на решение: спека пишется руками как источник истины или выводится из
|
## Критерии приёмки
|
||||||
кода. Для маленького API рукописная спека честнее — но это стоит обсудить.
|
|
||||||
|
- по спеке генерируется клиент, и сгенерированный клиент выполняет запрос к
|
||||||
|
живому сервису — оракул: прогон генератора плюс запрос сгенерированным
|
||||||
|
клиентом
|
||||||
|
- Swagger UI открывается и выполняет запрос **без внешней сети** — оракул:
|
||||||
|
запуск с отключённым интернетом
|
||||||
|
- маршрут, разошедшийся со спекой, красит гейт — оракул: намеренно
|
||||||
|
рассогласованный маршрут в прогоне гейта
|
||||||
|
- спека покрывает приём, каталог, точки, тренировки, записи и `/stats` —
|
||||||
|
оракул: сверка перечня путей спеки с таблицей маршрутов в `architecture.md`
|
||||||
|
|
||||||
|
## Рамки
|
||||||
|
|
||||||
|
Схема не трогается, данные только читаются, сервис перезапускается. Берётся
|
||||||
|
после того, как маршруты чтения существуют: спека рукописная, но описывать
|
||||||
|
нечего, пока конверт не задан.
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
# [goal] Разбор и хранилище
|
|
||||||
|
|
||||||
- **Секция:** порядок
|
|
||||||
- **Зачем:** Секции живого потока разобраны все, кроме словаря категориальных значений и пяти невиденных
|
|
||||||
- **Теги:** decomposed
|
|
||||||
|
|
||||||
Метрики, тренировки и записи со своими `id` разбираются и ложатся в часовые
|
|
||||||
объекты; тела перестали быть недифференцированной кучей.
|
|
||||||
|
|
||||||
Выведена из шага 3 плана. Сделано: разбор метрик в объекты, тренировки и
|
|
||||||
записи, `reindex`. Осталось: словарь категориальных значений и секции, которых
|
|
||||||
поток ещё не приносил.
|
|
||||||
|
|
||||||
Завершена, когда ни одна секция живого потока не числится неразобранной, а
|
|
||||||
категориальные значения имеют стабильный код рядом с переведённой строкой.
|
|
||||||
|
|
||||||
## Завершение
|
|
||||||
|
|
||||||
Ни одна секция живого потока не числится неразобранной, а категориальные
|
|
||||||
значения несут стабильный код рядом с переведённой строкой.
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# [goal] Полнота разбора потока
|
||||||
|
|
||||||
|
- **Секция:** темы
|
||||||
|
- **Зачем:** Пять секций HAE поток ещё не приносил, а ручные секции заводятся задним числом — разбор полон ровно до следующей новой формы
|
||||||
|
|
||||||
|
Тема: всё, что приезжает от источника, разобрано и доехало до витрины — не
|
||||||
|
только сегодня, но и после того, как источник изменится.
|
||||||
|
|
||||||
|
Выделена из цели «Разбор и хранилище», когда та достигла своего критерия
|
||||||
|
завершения: секции живого потока разобраны, категориальные значения несут
|
||||||
|
стабильный код. Осталось то, что заканчиваться не умеет по природе — источник
|
||||||
|
вправе прислать форму, которой раньше не было, а часть секций заводится
|
||||||
|
человеком задним числом.
|
||||||
|
|
||||||
|
В порядок не встаёт: работа приходит от потока, а не от плана. Первая встреча
|
||||||
|
новой секции наблюдаема (`healthlog uncovered` и `WARN` на свёртке) — тема
|
||||||
|
кормится этими событиями.
|
||||||
|
|
||||||
|
## Завершение
|
||||||
|
|
||||||
|
Завершена не бывает — это тема. Закрывается по мере того, как каждая приезжающая
|
||||||
|
форма доезжает до витрины, а не теряется между «принято» и «разобрано».
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Свёртка по сетке и предел размера ответа
|
||||||
|
|
||||||
|
- **Секция:** ядро
|
||||||
|
- **Зачем:** Враждебный запрос к каталогу стоил 693 мс и +153 МиБ кучи, а у точек множители те же и потолка нет ни у одного
|
||||||
|
- **Теги:** goal:read-api
|
||||||
|
|
||||||
|
Потребитель просит разбивку (`?from&to&bucket`) и получает свёртку по сетке —
|
||||||
|
либо честную ошибку вместо тихо подменённой сетки, — а сервис получает
|
||||||
|
названный предел на то, сколько он готов отдать за один запрос.
|
||||||
|
|
||||||
|
Вторая из трёх частей `read-api-points`. Берётся после
|
||||||
|
[конверта ответа](read-api-envelope-and-points.md): сетка — параметр того же
|
||||||
|
маршрута.
|
||||||
|
|
||||||
|
**Решение по размеру ответа принято, вариант «б».** Разбивка не задана и ответ
|
||||||
|
не влезает — сервер сам берёт сетку погрубее и **называет её в ответе**;
|
||||||
|
разбивка задана явно и не влезает — ошибка со списком доступных сеток, а не
|
||||||
|
тихая подмена. Различие существенно: иначе агент, попросивший минутную сетку,
|
||||||
|
получит суточные суммы и не узнает об этом.
|
||||||
|
|
||||||
|
**Порог неполного ведра решается здесь, и вместе с ним — его полярность.**
|
||||||
|
Измерению рода агрегации порог не понадобился, свёртке в ответе он нужен, а
|
||||||
|
готовые решения задают его **противоположно**: Graphite `xFilesFactor` — доля
|
||||||
|
обязательно известных точек (умолчание 0.5 при роллапе и 0 при рендере, один
|
||||||
|
параметр с двумя умолчаниями), RRDtool `xff` — доля допустимо неизвестных. Обе
|
||||||
|
величины выглядят как «0.5», означая разное; полярность придётся назвать вслух в
|
||||||
|
`architecture.md`, иначе через полгода два места кода поймут поле по-разному.
|
||||||
|
|
||||||
|
**Цена измерена, и она унаследована.** На каталоге враждебный запрос
|
||||||
|
(20 метрик × 8 часов × 5000 точек) дал 693 мс и +153 МиБ живой кучи, при том что
|
||||||
|
приём в том же процессе уже даёт пик 768 МиБ на теле 40 МиБ. Условный запрос
|
||||||
|
снял повтор, но первый запрос стоит столько же, а множители «метрики × окно ×
|
||||||
|
точки × одновременные запросы» по-прежнему без потолка. Сюда же уезжают
|
||||||
|
отложенные варианты задачи «цена читающего маршрута»: собственный дедлайн
|
||||||
|
маршрута и потоковое измерение по метрике — второе только если счётчик
|
||||||
|
заговорит.
|
||||||
|
|
||||||
|
Инвариант, который здесь легче всего нарушить: **нижний слой HAE не
|
||||||
|
суммируется** ни при какой сетке — это интерполяция, а не сэмплы.
|
||||||
|
|
||||||
|
## Критерии приёмки
|
||||||
|
|
||||||
|
- «шаги за неделю по дням» отвечаются одним запросом, и в ответе названа
|
||||||
|
фактическая сетка — оракул: запрос к поднятому сервису на живом архиве
|
||||||
|
- явно заданная сетка, которая не влезает в предел, даёт ошибку со списком
|
||||||
|
доступных сеток, а не подменённый ответ — оракул: тест
|
||||||
|
- неполное ведро обрабатывается объявленным порогом, и полярность порога
|
||||||
|
названа в `docs/architecture.md` вслух — оракул: тест на границе плюс глазами
|
||||||
|
по разделу
|
||||||
|
- нижний слой HAE не суммируется ни при какой сетке — оракул: тест на
|
||||||
|
накопительной метрике, у которой есть и нижний, и часовой слой
|
||||||
|
|
||||||
|
## Рамки
|
||||||
|
|
||||||
|
Схема не трогается, данные только читаются, сервис перезапускается. Берётся
|
||||||
|
после конверта ответа. Против `./data` — только `task up` / `task run`.
|
||||||
|
|
||||||
|
Связано: `docs/architecture.md` → «Свёртка и размер ответа»; идея
|
||||||
|
[NDJSON-потока](ndjson-stream.md) закрывает другую сторону того же предела.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Конверт ответа и точки за период
|
||||||
|
|
||||||
|
- **Секция:** ядро
|
||||||
|
- **Зачем:** Точки лежат в витрине и наружу не отдаются: ни один потребитель не может спросить «вес за год» иначе как sqlite3 на хосте
|
||||||
|
- **Теги:** goal:read-api
|
||||||
|
|
||||||
|
Потребитель получает значения метрики за период одним запросом
|
||||||
|
`?from&to`, и из ответа видно, что именно ему отдали: слой, род свёртки и
|
||||||
|
границу окна, в котором род измерен.
|
||||||
|
|
||||||
|
Первая из трёх частей, на которые разложена `read-api-points`. Здесь решается
|
||||||
|
**конверт** — форма, которую унаследуют все остальные маршруты чтения и оба
|
||||||
|
транспорта.
|
||||||
|
|
||||||
|
**Форма провода решается здесь, один раз.** Сегодня типы `internal/catalog`
|
||||||
|
сами несут json-теги, а транспорт владеет только обёрткой: переименование поля
|
||||||
|
в домене меняет публичный контракт без касания `httpapi`, и держит это один
|
||||||
|
байтовый тест непустого ответа. Либо объявить в `architecture.md`, что типы
|
||||||
|
чтения и есть форма провода для всех транспортов (HTTP и MCP отдают её байт в
|
||||||
|
байт), либо завести DTO в транспорте. Выбрать надо **до** того, как образец
|
||||||
|
скопируют соседние задачи.
|
||||||
|
|
||||||
|
**Машинерия условного запроса готова — её берут, а не пишут заново.**
|
||||||
|
`store.VersionedRead` держит правило «версией, снятой после чтения, не
|
||||||
|
подписывать»; `httpapi` умеет `If-None-Match` и `304`. Новое здесь одно: метка
|
||||||
|
обязана нести **область действия** — у точек ответ есть функция параметров
|
||||||
|
запроса, поэтому `etag(scope, version)` требует их канонизированной формы, иначе
|
||||||
|
`304` ответит на другой набор данных. Детали — `docs/architecture.md`,
|
||||||
|
«Условный запрос».
|
||||||
|
|
||||||
|
**Клиент обязан видеть границы окна измерения.** Род метрики измерен по 48 самым
|
||||||
|
свежим **общим** часам, а не по последним 48 часам календаря: выключенная
|
||||||
|
минутная автоматизация HAE останавливает пополнение множества общих часов, и
|
||||||
|
окно замирает, продолжая объявлять род. Единственный след — `last_hour` в
|
||||||
|
ответе; конверт обязан его нести.
|
||||||
|
|
||||||
|
## Критерии приёмки
|
||||||
|
|
||||||
|
- «вес за год» отвечается одним запросом без доступа к файлу базы — оракул:
|
||||||
|
запрос к поднятому сервису на живом архиве
|
||||||
|
- в ответе всегда видны `layer`, `aggregation` и `last_hour` — оракул: тест на
|
||||||
|
форме ответа
|
||||||
|
- повторный запрос с `If-None-Match` даёт `304`, а тот же запрос с другими
|
||||||
|
параметрами — `200` с другим телом — оракул: тест на паре запросов с разной
|
||||||
|
областью действия метки
|
||||||
|
- форма провода объявлена в `docs/architecture.md` — доменные типы или DTO, —
|
||||||
|
и соседние маршруты ссылаются на это решение, а не повторяют выбор — оракул:
|
||||||
|
глазами по разделу
|
||||||
|
|
||||||
|
## Рамки
|
||||||
|
|
||||||
|
Схема не трогается, данные только читаются, сервис перезапускается. Против
|
||||||
|
`./data` — только `task up` / `task run`.
|
||||||
|
|
||||||
|
Связано: `docs/architecture.md` → «Read API», «Условный запрос», «Измерение рода
|
||||||
|
агрегации».
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
# Read API: точки, выбор слоя, свёртка по сетке
|
|
||||||
|
|
||||||
- **Секция:** ядро
|
|
||||||
- **Зачем:** Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может
|
|
||||||
- **Теги:** goal:read-api
|
|
||||||
|
|
||||||
Сейчас данные достаются только `sqlite3` на хосте. Все три сценария —
|
|
||||||
агент-медик, трекер тренировок, фитнес-игра — упираются в отсутствие чтения.
|
|
||||||
|
|
||||||
Формы запроса ровно две, и это один запрос с необязательным параметром:
|
|
||||||
`?from&to` — все значения за период (вес, лекарства, симптомы), `?from&to&bucket`
|
|
||||||
— с разбивкой (шаги, энергия).
|
|
||||||
|
|
||||||
Решение по размеру ответа (вариант «б»): разбивка не задана и ответ не влезает —
|
|
||||||
сервер сам берёт сетку погрубее и **называет её в ответе**; разбивка задана явно
|
|
||||||
и не влезает — ошибка со списком доступных сеток, а не тихая подмена. Различие
|
|
||||||
существенно: иначе агент, попросивший минутную сетку, получит суточные суммы.
|
|
||||||
|
|
||||||
**Отдача тренировок и записей входит сюда же.** Разбор и хранение сущностей с
|
|
||||||
собственным `id` сделаны (change `2026-08-02-trenirovki-i-zapisi`), а эндпоинтов
|
|
||||||
нет: тренировка с маршрутом и записи `stateOfMind` лежат в витрине и наружу не
|
|
||||||
отдаются. Вводить их раньше конверта ответа значило бы задать контракт
|
|
||||||
мимоходом, поэтому `GET /workouts`, `GET /workouts/{id}` и
|
|
||||||
`GET /records/{kind}` закрываются этой задачей — вместе с формой конверта и
|
|
||||||
правилом размера ответа. Второй сценарий паспорта (трекер) до тех пор не закрыт.
|
|
||||||
|
|
||||||
Готово, когда «шаги за неделю по дням» и «вес за год» отвечаются одним запросом
|
|
||||||
каждый, тренировка отдаётся одним пакетом вместе с маршрутом, а в ответе всегда
|
|
||||||
видно `layer`, `bucket` и `aggregation`.
|
|
||||||
|
|
||||||
**Порог неполного ведра решается здесь, и вместе с ним — его полярность.**
|
|
||||||
Каталог и род агрегации сделаны (change `2026-08-02-katalog-i-rod-agregacii`), и
|
|
||||||
измерению порог заполненности не понадобился: у него две конкурирующие гипотезы,
|
|
||||||
и неполный час не сходится ни с одной сам собой. Свёртке в ответе он нужен, а
|
|
||||||
готовые решения задают его **противоположно**: Graphite `xFilesFactor` — доля
|
|
||||||
обязательно известных точек (умолчание 0.5 при роллапе и 0 при рендере, один
|
|
||||||
параметр с двумя умолчаниями), RRDtool `xff` — доля допустимо неизвестных. Обе
|
|
||||||
величины выглядят как «0.5», означая разное; полярность придётся назвать вслух в
|
|
||||||
`architecture.md`, иначе через полгода два места кода поймут поле по-разному.
|
|
||||||
|
|
||||||
**Предел размера ответа тоже здесь, и он унаследовал измеренную цену.** У
|
|
||||||
каталога предела нет намеренно: правило размера — общее для маршрутов чтения, и
|
|
||||||
задавать его мимоходом на первой ручке значило бы решить контракт до того, как
|
|
||||||
известна форма тяжёлого ответа. Каталог станет первым его потребителем.
|
|
||||||
|
|
||||||
Цена измерена на каталоге (задача «цена читающего маршрута», закрыта чекпойнтом
|
|
||||||
WAL и условным запросом): 693 мс и +153 МиБ живой кучи на враждебном запросе
|
|
||||||
(20 метрик × 8 часов × 5000 точек), при том что приём в том же процессе уже даёт
|
|
||||||
пик 768 МиБ на теле 40 МиБ. Условный запрос снял повтор, но первый запрос стоит
|
|
||||||
столько же, а множители «метрики × окно × точки × одновременные запросы»
|
|
||||||
по-прежнему без потолка. Сюда же уезжают отложенные варианты той задачи:
|
|
||||||
собственный дедлайн маршрута и потоковое измерение по метрике (второе — только
|
|
||||||
если счётчик заговорит).
|
|
||||||
|
|
||||||
**Машинерия условного запроса готова, и её надо взять, а не написать заново.**
|
|
||||||
`store.VersionedRead` держит правило «версией, снятой после чтения, не
|
|
||||||
подписывать»; `httpapi` — разбор `If-None-Match` и `304`. Метка обязана нести
|
|
||||||
**область действия**: у точек ответ есть функция параметров запроса, и
|
|
||||||
`etag(scope, version)` требует их канонизированную форму — иначе `304` ответит
|
|
||||||
на другой набор данных. Детали — `docs/architecture.md`, «Условный запрос».
|
|
||||||
|
|
||||||
**Форма провода наследуется от каталога, и это надо решить один раз.** Сегодня
|
|
||||||
типы `internal/catalog` сами несут json-теги, а транспорт владеет только
|
|
||||||
обёрткой: переименование поля в домене меняет публичный контракт без касания
|
|
||||||
`httpapi`. Держит это один байтовый тест непустого ответа. Либо объявить в
|
|
||||||
`architecture.md`, что типы чтения и есть форма провода для всех транспортов
|
|
||||||
(HTTP и MCP отдают её байт в байт), либо завести DTO в транспорте — но выбрать до
|
|
||||||
того, как образец скопирует эта задача.
|
|
||||||
|
|
||||||
**Клиент обязан смотреть на границы окна измерения.** Род метрики измерен по
|
|
||||||
48 самым свежим ОБЩИМ часам, а не по последним 48 часам календаря: если минутная
|
|
||||||
автоматизация HAE выключена, множество общих часов не пополняется и окно
|
|
||||||
замирает. Род при этом продолжает объявляться, и единственный след — `last_hour`
|
|
||||||
в ответе. Правило выбора свёртки в Read API обязано это учитывать (или явно
|
|
||||||
объявить, что не учитывает).
|
|
||||||
|
|
||||||
Связано: `docs/architecture.md` → «Read API», «Измерение рода агрегации»,
|
|
||||||
план → шаг «Read API».
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Тренировки и записи наружу
|
||||||
|
|
||||||
|
- **Секция:** ядро
|
||||||
|
- **Зачем:** Тренировки с маршрутами и stateOfMind разобраны и лежат в витрине, а эндпоинтов нет — второй сценарий паспорта не закрыт
|
||||||
|
- **Теги:** goal:read-api
|
||||||
|
|
||||||
|
Трекер тренировок забирает тренировку одним пакетом вместе с маршрутом, а
|
||||||
|
записи `stateOfMind` достаются за период — без доступа к файлу базы.
|
||||||
|
|
||||||
|
Третья из трёх частей `read-api-points`. Разбор и хранение сущностей с
|
||||||
|
собственным `id` сделаны (change `2026-08-02-trenirovki-i-zapisi`), эндпоинтов
|
||||||
|
нет: данные лежат в витрине и наружу не отдаются. Второй сценарий паспорта до
|
||||||
|
тех пор не закрыт.
|
||||||
|
|
||||||
|
Маршруты: `GET /workouts`, `GET /workouts/{id}`, `GET /records/{kind}`.
|
||||||
|
|
||||||
|
**Конверт не изобретается заново** — наследуется от
|
||||||
|
[конверта ответа](read-api-envelope-and-points.md) вместе с формой провода и
|
||||||
|
условным запросом. Вводить эти маршруты раньше конверта значило бы задать
|
||||||
|
контракт мимоходом; именно поэтому они здесь, а не в задаче о разборе.
|
||||||
|
|
||||||
|
**Маршрут — первый по-настоящему тяжёлый ответ проекта.** Он лежит блобом внутри
|
||||||
|
тренировки, и правило размера ответа
|
||||||
|
([свёртка по сетке](read-api-bucketing.md)) обязано распространяться и на него —
|
||||||
|
иначе одна тренировка с длинным треком проходит мимо предела, названного для
|
||||||
|
точек. Разворачивание маршрута в отдельную таблицу остаётся идеей и этой задачей
|
||||||
|
не решается.
|
||||||
|
|
||||||
|
## Критерии приёмки
|
||||||
|
|
||||||
|
- тренировка отдаётся одним пакетом вместе с маршрутом — оракул: запрос к
|
||||||
|
поднятому сервису на живом архиве
|
||||||
|
- записи `stateOfMind` за период достаются `GET /records/{kind}` — оракул: тест
|
||||||
|
на пакетах `internal/hae/testdata`
|
||||||
|
- конверт, форма провода и условный запрос те же, что у точек, а не собственные
|
||||||
|
— оракул: тест, сравнивающий форму ответа обоих маршрутов
|
||||||
|
- тренировка с длинным маршрутом подчиняется тому же пределу размера ответа,
|
||||||
|
что и точки — оракул: тест на синтетическом треке за пределом
|
||||||
|
|
||||||
|
## Рамки
|
||||||
|
|
||||||
|
Схема не трогается, данные только читаются, сервис перезапускается. Берётся
|
||||||
|
после конверта ответа. Против `./data` — только `task up` / `task run`.
|
||||||
|
|
||||||
|
Связано: `docs/architecture.md` → «Read API»; идея
|
||||||
|
[разворачивания маршрутов в таблицу](workout-routes-table.md).
|
||||||
@@ -1,19 +1,26 @@
|
|||||||
# [goal] Read API
|
# [goal] Чтение данных клиентами
|
||||||
|
|
||||||
- **Секция:** порядок
|
- **Секция:** порядок
|
||||||
- **Зачем:** Данные видны только через sqlite на хосте — ни один из трёх потребителей ничего прочитать не может
|
- **Зачем:** Данные видны только через sqlite на хосте — ни один из трёх потребителей, включая агента-медика, ничего прочитать не может
|
||||||
- **Теги:** decomposed
|
- **Теги:** decomposed
|
||||||
|
|
||||||
Потребители читают точки: выбор слоя, свёртка по сетке, предел размера ответа.
|
Потребители читают данные: точки с выбором слоя и свёрткой по сетке, тренировки
|
||||||
|
и записи, машиночитаемый контракт — и всё то же самое через MCP.
|
||||||
|
|
||||||
Выведена из шага 5 плана. Идёт после каталога и рода агрегации намеренно: без
|
Выведена из шагов 5 и 7 плана. Идёт после каталога и рода агрегации намеренно:
|
||||||
измеренного рода свёртка в ответе неотличима от угадывания, а ошибиться здесь
|
без измеренного рода свёртка в ответе неотличима от угадывания, а ошибиться
|
||||||
дорого — просуммировать нижний слой значит завысить втрое.
|
здесь дорого — просуммировать нижний слой значит завысить втрое.
|
||||||
|
|
||||||
Завершена, когда любой из трёх потребителей получает точки за период без
|
**MCP входит в эту цель, а не идёт отдельной.** Прежде их было две, и разделяла
|
||||||
доступа к файлу базы.
|
их очередь: адаптер собственной логики не несёт, он переводит вызовы в те же
|
||||||
|
обработчики, и переводить было нечего. Очередь никуда не делась — она стала
|
||||||
|
порядком задач внутри цели, — а вот отдельная цель под адаптер описывала не
|
||||||
|
направление, а последний шаг этого же направления. Заказчик у обоих транспортов
|
||||||
|
один: три потребителя, из которых первый — агент.
|
||||||
|
|
||||||
## Завершение
|
## Завершение
|
||||||
|
|
||||||
Любой из трёх потребителей получает точки за период без доступа к файлу базы,
|
Любой из трёх потребителей получает точки, тренировки и записи за период без
|
||||||
и предел размера ответа объявлен, а не подразумевается.
|
доступа к файлу базы; предел размера ответа объявлен, а не подразумевается;
|
||||||
|
агент-медик читает то же самое через MCP тем же токеном чтения, и собственной
|
||||||
|
логики адаптер не несёт.
|
||||||
|
|||||||
Reference in New Issue
Block a user