заведены задачи из урожая ревью и заметок владельца

This commit is contained in:
av
2026-08-12 09:01:31 +03:00
parent 5f1273292a
commit 07433bb9f9
21 changed files with 355 additions and 0 deletions
+9
View File
@@ -53,3 +53,12 @@
- [🧹 Разобрать мелочи http-транспорта](items/http-transport-nits.md) — Маршруты зарегистрированы дважды, и переименование пути в main.go проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом.
- [🧹 Переименовать образец конфига в config.example.toml](items/config-example-toml.md) — Конвенция называет config.dist.toml объявленным расхождением, но тут же пишет это имя как правило — документ противоречит сам себе, а образец расходится с конвенцией.
- [🔬 Стоит ли брать OpenTelemetry вместо голого Prometheus](items/opentelemetry-fit.md) — Метрик одиннадцать штук на пять счётчиков, трассировки нет вовсе: путь одной записи по конвейеру собирается только чтением логов глазами.
- [🧹 Обновить Go до 1.26 в модуле и в образе](items/go-1-26-upgrade.md) — Модуль объявляет go 1.25.0, образ собирается на golang:1.25-alpine, а на машине разработки стоит 1.26.5: три места расходятся, и проверки идут не на той версии, что уезжает на сервер.
- [🧹 Ловить уязвимости в зависимостях шагом гейта](items/gate-dependency-vulnerabilities.md) — govulncheck находит две достижимые уязвимости в клиентах Yandex, а ни гейт, ни список «чего в гейте нет» о нём не знают: узнать о третьей будет неоткуда.
- [🧹 Сверять версию Go в образе с директивой go.mod](items/gate-go-version-sync.md) — Образ гейт не собирает, и расхождение версий прошло весь конвейер молча: task image упал бы уже на сервере.
- [🧹 Считать покрытие изменённых строк шагом гейта](items/gate-changed-lines-coverage.md) — Свойство «изменённое место покрыто хоть одним тестом» записано в docs/review.md, но не механизировано: за две задачи подряд непокрытые шаги ловили руками.
- [🧹 Прервать шаг конвейера отменой контекста](items/context-cancel-in-pipeline.md) — Воркер читает ctx только между итерациями: остановка контейнера ждёт конца шага, а на занятом писателе один запрос к хранилищу держится до 9,5 секунды при мягком таймауте в 5.
- [🧹 Разобрать мелочи слоя хранилища](items/storage-layer-nits.md) — Три мелочи ниже потолка триажа: цикл воркера пишет потерю захвата уровнем ERROR и считает её отказом, тип ошибки заведён там, где конвенция просит sentinel, а FileName несёт два разных смысла.
- [🔬 Уведомление SpeechKit о готовности вместо опроса](items/speechkit-callback-fit.md) — Шаг проверки дёргает операцию раз в 5 секунд всё время распознавания: часовая запись даёт порядка 720 обращений к платному сервису вместо одного ответа.
- [🔬 Адрес объекта в тексте отказа SpeechKit](items/speechkit-error-text-leak.md) — Текст отказа операции приходит от Yandex и уезжает в журнал и в колонку error_text: если он несёт URI объекта, из журнала снова собирается ссылка на чужую запись.
- [🔬 Квота по общему размеру загруженного на пользователя](items/per-user-size-quota.md) — Паспорт и security.md запрещают отказы по квоте пользователю, а заметка владельца просит квоту по умолчанию 5 ГБ — открытое противоречие с границей домена, которое владелец решил не разбирать сейчас.
+47
View File
@@ -0,0 +1,47 @@
# 🧹 Прервать шаг конвейера отменой контекста
- **Тип:** chore
- **Категория:** Очередь
- **Зачем:** Воркер читает ctx только между итерациями: остановка контейнера ждёт конца шага, а на занятом писателе один запрос к хранилищу держится до 9,5 секунды при мягком таймауте в 5.
Долг назван в `docs/architecture.md` — раздел про capability `pipeline`, где
отмена контекста посреди шага перечислена среди неописанного. Ревью задачи
`pocketbase-storage` 2026-08-12 его уточнило, и уточнение меняет способ починки:
**передать `ctx` по всему коду недостаточно**. Хранилище не принимает контекста ни в
одном из методов записи — ни в сохранении записи, ни в транзакции, ни в чтении по
идентификатору; отменять там нечем без перехода части записи на сырые запросы,
у которых контекст есть.
Число снято на прогоне: под внешней эксклюзивной блокировкой файла базы захват
задачи держится 9,53 секунды (таймаут занятости хранилища — 10 секунд) и
возвращает отличимый отказ, а не «работы нет».
## Затрагивает
- цикл воркера `internal/controller/worker`;
- сигнатуры шагов конвейера в `internal/service`;
- договор ядра с хранилищем `internal/contract` и его реализация в
`internal/adapter/repo/pocketbase`;
- таймауты мягкой и жёсткой остановки — конфиг, `[server] shutdown_timeout` и
`force_shutdown_timeout`;
- спека `pipeline`: сегодня раздел `Purpose` объявляет отмену контекста
неописанной.
## Критерии приёмки
- Шаг, идущий дольше мягкого таймаута, обрывается по сигналу остановки. Оракул —
тест с шагом, спящим дольше таймаута: остановка укладывается в бюджет, а не
доживает до жёсткой.
- Оборванный шаг оставляет задачу пригодной к повтору. Оракул — тот же тест:
после остановки задача не захвачена и выдаётся следующему.
- Отмена не путается с отказом: прерванный шаг не тратит попытку и не уходит в
«мертва». Оракул — тест на трёх прерываниях подряд, счётчик попыток не растёт
до предела.
- Спека `pipeline` описывает отмену, и раздел `Purpose` её больше не числит
неописанной. Оракул — `openspec validate --strict`.
## Рамки
Таймаут занятости хранилища этой задачей не трогаем: он свойство библиотеки, а не
наше. Мягкая остановка в 5 секунд может оказаться меньше неотменяемого минимума —
тогда число меняется, и это решение человека.
+1
View File
@@ -3,6 +3,7 @@
- **Тип:** goal
- **Секция:** Направления
- **Зачем:** Хранение бессрочное, а способа убрать запись нет ни одного: ошибочно загруженный файл и разговор, который человек не хочет держать у нас, остаются навсегда.
- **Теги:** decomposed
Сервис объявлен архивом 2026-08-11, и с тем же решением у человека появляется
обратное право: сказать «убери это» и убедиться, что убрано. Речь в записи
@@ -0,0 +1,42 @@
# 🧹 Считать покрытие изменённых строк шагом гейта
- **Тип:** chore
- **Категория:** Очередь
- **Зачем:** Свойство «изменённое место покрыто хоть одним тестом» записано в docs/review.md, но не механизировано: за две задачи подряд непокрытые шаги ловили руками.
- **Теги:** question
`CLAUDE.md`, раздел «Гейт», объявляет прямо: «покрытие изменённых строк не
считается ничем». Цена этого измерена дважды. В задаче
`http-handler-tests-never-green` тесты обработчика не были зелёными ни разу; в
`pocketbase-storage` два из трёх шагов конвейера переписали целиком и не
выполнили ни одним тестом — нашёл это проход ревью, а не машина.
## Затрагивает
- набор шагов `task gate` в `Taskfile.yml` и переменная `BASE` как база диффа;
- семантика гейта в `CLAUDE.md`, раздел «Гейт», строка про покрытие;
- `docs/review.md`, раздел настройки конвейера: чем проход `autotests` перестаёт
заниматься руками.
## Критерии приёмки
- Изменённая строка без покрытия роняет гейт. Оракул — прогон на дереве, где в
тронутый файл добавлена заведомо невыполняемая ветка: шаг краснеет с её
адресом.
- Изменение, не трогающее код, шаг не гоняет. Оракул — `task gate` на дереве с
правкой одной только документации: шаг сообщает о пропуске с причиной.
- Порог назван числом в `CLAUDE.md` и совпадает с тем, что проверяет шаг. Оракул
`task gate`, шаг `docs.py check`.
## Вопросы
- **Какой порог и что считать изменённой строкой.** Варианты: сто процентов
изменённых строк (роняет на всякой ветке отказа, которую нечем изобразить),
порог ниже ста (число берётся из ниоткуда), только новые функции целиком.
Цена решения — частота ложных отказов гейта; пока решения нет, шаг заводить
нельзя.
## Рамки
Общее покрытие проекта не считаем и порога на него не ставим: он растёт от
тестов на тривиальное и не отвечает ни на один вопрос.
@@ -0,0 +1,35 @@
# 🧹 Ловить уязвимости в зависимостях шагом гейта
- **Тип:** chore
- **Категория:** Очередь
- **Зачем:** govulncheck находит две достижимые уязвимости в клиентах Yandex, а ни гейт, ни список «чего в гейте нет» о нём не знают: узнать о третьей будет неоткуда.
Обе найдены ревью задачи `pocketbase-storage` 2026-08-12 и достижимы из кода
распознавателя: `GO-2026-6061` в `google.golang.org/grpc` (исправлено в v1.82.1)
и `GO-2026-5764` в `aws-sdk-go-v2``aws/protocol/eventstream` (в v1.7.8) и
`service/s3` (в v1.97.3). Ни одну из них эта задача не вносила: `git diff
origin/master -- go.mod` их версий не касался.
## Затрагивает
- версии `google.golang.org/grpc`, `aws-sdk-go-v2/service/s3` и
`aws-sdk-go-v2/aws/protocol/eventstream` в `go.mod`;
- набор шагов `task gate` в `Taskfile.yml`;
- семантика гейта в `CLAUDE.md` — раздел «Гейт», список «что красит безусловно»
либо «чего в гейте намеренно нет».
## Критерии приёмки
- Обе объявленные уязвимости из кода недостижимы. Оракул — `govulncheck ./...`
печатает «No vulnerabilities found» либо оставляет только недостижимые.
- Новая достижимая уязвимость роняет гейт. Оракул — прогон `task gate` на
дереве с намеренно откаченной версией `grpc`: шаг краснеет с именем уязвимости.
- Место шага названо в `CLAUDE.md`: либо в перечне красящих безусловно, либо в
«чего в гейте нет» с указанием, кто тогда обязан его гонять. Оракул —
`task gate`, шаг `docs.py check`.
## Рамки
Обновление версий трогает клиентов Yandex — прогон на реальных ключах запрещён,
подставляй `internal/adapter/recognizer/memory.go`. Решение, ронять ли гейт на
уязвимости без исправления, принимает человек.
+35
View File
@@ -0,0 +1,35 @@
# 🧹 Сверять версию Go в образе с директивой go.mod
- **Тип:** chore
- **Категория:** Очередь
- **Зачем:** Образ гейт не собирает, и расхождение версий прошло весь конвейер молча: task image упал бы уже на сервере.
Случай записан журналом дефектов `docs/review.md` за 2026-08-12: `go mod tidy`
поднял директиву до 1.25, `Dockerfile` остался на `golang:1.24-alpine` с
`GOTOOLCHAIN=local`, и шесть проходов ревью вместе с восемью шагами гейта
показали зелёное. Нашлось случайным свойством прогона, а не устройством
проверок.
Сборку образа в гейт не заводим — она дорога. Дешёвая замена: сравнение двух
строк.
## Затрагивает
- набор шагов `task gate` в `Taskfile.yml`;
- директива `go` в `go.mod` и сборочный слой `Dockerfile` как источники
сравнения;
- семантика гейта в `CLAUDE.md`, раздел «Гейт».
## Критерии приёмки
- Расхождение версий роняет гейт. Оракул — прогон `task gate` на дереве, где
версия в `Dockerfile` понижена на минор: шаг краснеет и называет оба числа.
- Совпадение гейт не роняет. Оракул — `task gate` на неизменённом дереве.
- Шаг не требует docker и работает без сети. Оракул — прогон с
`DOCKER_HOST=/dev/null`.
## Рамки
Сравнение строковое: `Dockerfile` называет `golang:<major>.<minor>-alpine`, а
`go.mod``<major>.<minor>.<patch>`. Сверяются мажор и минор; патч сборочного
образа своей жизнью живёт законно.
+30
View File
@@ -0,0 +1,30 @@
# 🧹 Обновить Go до 1.26 в модуле и в образе
- **Тип:** chore
- **Категория:** Очередь
- **Зачем:** Модуль объявляет go 1.25.0, образ собирается на golang:1.25-alpine, а на машине разработки стоит 1.26.5: три места расходятся, и проверки идут не на той версии, что уезжает на сервер.
Заметка владельца от 2026-08-12. Версия 1.25 в модуле появилась не выбором, а
требованием PocketBase: `go mod tidy` поднял директиву сам, а образ подтянули
следом. То есть сегодняшнее число никем не назначено, и повторится это на
следующей же зависимости.
## Затрагивает
- директива `go` в `go.mod`;
- сборочный слой `Dockerfile`;
- строка стека в `CLAUDE.md` и в `README.md`.
## Критерии приёмки
- Модуль, образ и документы называют одну версию Go. Оракул — `grep` по
четырём местам: `go.mod`, `Dockerfile`, `CLAUDE.md`, `README.md`; все дают
одно число.
- Сборка на объявленной версии проходит. Оракул — `task image` и
`CGO_ENABLED=0 go build ./...` на чистом дереве.
- Гейт зелёный целиком. Оракул — `task gate`.
## Рамки
Версию выбирает человек: обновление до неподдерживаемой ещё зависимостями версии
ломает сборку молча. Выкладку не запускаем.
+7
View File
@@ -19,6 +19,13 @@
и заливка в Object Storage, — и сколько времени и места на диске занимает
шестичасовая запись на каждом из них.
**Звено, добавленное ревью 2026-08-12:** приём пишет тело записи на диск
дважды — разбор формы записывает во временный каталог всё, что не поместилось в
память, выделенную под multipart, а следом рабочая копия переливает то же
содержимое в свой файл. Верхняя
оценка на один запрос — два объявленных потолка записи (`entity.MaxRecordSize`,
8 ГиБ). Числа нет: самая длинная проверенная запись — 9,6 МБ.
## Куда ляжет ответ
`docs/research/intake-limits.md` — числами и с командой замера по каждому звену.
+1
View File
@@ -3,6 +3,7 @@
- **Тип:** goal
- **Секция:** Запланировано
- **Зачем:** У задачи нет владельца, а HTTP API открыт наружу без аутентификации: пригласить второго человека сейчас значит открыть ему чужие расшифровки.
- **Теги:** decomposed
Приложение узнаёт, кто к нему пришёл, и показывает каждому только его записи.
Учётные записи заводит и проверяет внешний провайдер — Authelia по OIDC; своей
+24
View File
@@ -0,0 +1,24 @@
# 🔬 Квота по общему размеру загруженного на пользователя
- **Тип:** research
- **Категория:** Очередь
- **Зачем:** Паспорт и security.md запрещают отказы по квоте пользователю, а заметка владельца просит квоту по умолчанию 5 ГБ — открытое противоречие с границей домена, которое владелец решил не разбирать сейчас.
Заметка звучит так: «Квота по размеру загруженных файлов на пользователя. В
конфиге устанавливать квоту по общему размеру, по дефолту не больше 5GB».
**Взять в работу её нельзя, пока не решён спор с паспортом.**
`docs/passport.md`, раздел «Что целью не является», говорит прямо: «Цен, счетов
и отказов по исчерпании квоты не делаем: пользователя, потратившего слишком
много, останавливает разговор или отзыв доступа в Authelia». То же повторяет
`docs/security.md`, «Что вне модели»: квот нет и не будет.
Владелец 2026-08-12 решил не двигать границу сейчас, а разобрать позже. Разбор
отвечает на три вещи: двигается ли граница паспорта; отказывает ли сервис
пользователю или только оповещает владельца; на чём считается объём, раз учёта
по пользователям сегодня нет вовсе (задача `usage-accounting`).
## Рамки
Пока раздел «Вопрос» пуст, запись числится сырьём и в работу не берётся. Правка
паспорта — решение человека: это граница домена, а не деталь.
+1
View File
@@ -3,6 +3,7 @@
- **Тип:** goal
- **Секция:** Запланировано
- **Зачем:** Расшифровка занимает минуты, и всё это время человек либо смотрит на экран с опросом статуса, либо забывает вернуться.
- **Теги:** decomposed
Задача уходит в работу — человек закрывает приложение и получает сообщение,
когда текст готов. Пользователь Telegram это уже имеет: бот отвечает сам.
+8
View File
@@ -30,6 +30,14 @@
экрана. Оракул — тест: подставной API отвечает `transcribe`, затем `done`;
экран переходит к тексту сам.
**Учесть от ревью 2026-08-12:** имя файла в хранилище несёт хвост имени, данного
отправителем, — расширение берётся из него дословно, и запись
`разговор.тайное-слово` ложится на диск именем с этим хвостом. Сегодня наружу
оно не выходит: изъятие из инварианта приватности кончается журналом владельца.
Экран списка это меняет — имя попадает в ссылку на скачивание и в заголовок
ответа, то есть на страницу. Выхода за каталог хранения при этом нет, проверено
пятнадцатью враждебными именами.
## Рамки
Поиска по тексту, переименования и удаления записей не делаем. Правки текста не
+1
View File
@@ -3,6 +3,7 @@
- **Тип:** goal
- **Секция:** Сопровождение
- **Зачем:** Отказ замечает пользователь, а не владелец: оповещения нет, а путь записи по конвейеру собирается глазами по логам контейнера.
- **Теги:** decomposed
Наблюдаем **мы**, а не пользователь сервиса, — поэтому цель стоит в
сопровождении, а не среди возможностей приложения. Пользовательская половина
+32
View File
@@ -0,0 +1,32 @@
# 🔬 Уведомление SpeechKit о готовности вместо опроса
- **Тип:** research
- **Категория:** Очередь
- **Зачем:** Шаг проверки дёргает операцию раз в 5 секунд всё время распознавания: часовая запись даёт порядка 720 обращений к платному сервису вместо одного ответа.
Заметка владельца от 2026-08-12. Сегодня готовность узнаётся только опросом:
воркер проверки берёт задачу, спрашивает операцию и ставит задержку в 5 секунд —
и так до конца распознавания. Числа задержек стоят в `docs/database.md`,
«Настройки с числовым значением».
## Вопрос
Умеет ли отложенное распознавание SpeechKit сообщать о готовности само —
вызовом на наш адрес, очередью или иным способом, — и что это меняет для
сервиса: остаётся ли воркер проверки, чем закрывается пропущенное уведомление и
какой вход появляется снаружи.
## Куда ляжет ответ
- Наблюдение с командой и условиями — записка в `docs/research/`.
- Появляется вход снаружи — строка в `docs/security.md`, раздел «Недоверенный
вход», и в `docs/architecture.md`, «Внешние границы и форматы».
- Выбранный способ — решением в `docs/adr/`, потому что опрос он заменяет
целиком; реализация — отдельной задачей.
## Рамки
Прогон на реальных ключах Yandex запрещён — ответ ищется в документации сервиса
и, если понадобится замер, отдельным разрешением человека с названной ценой.
Уведомление требует адреса, доступного снаружи: сегодня он есть, но закрывать
его будет `oidc-login`, и это учитывается в ответе.
+33
View File
@@ -0,0 +1,33 @@
# 🔬 Адрес объекта в тексте отказа SpeechKit
- **Тип:** research
- **Категория:** Очередь
- **Зачем:** Текст отказа операции приходит от Yandex и уезжает в журнал и в колонку error_text: если он несёт URI объекта, из журнала снова собирается ссылка на чужую запись.
Гипотеза ревью задачи `pocketbase-storage` 2026-08-12, оставшаяся без оракула:
поднять настоящий отказ Yandex нечем, а прогон на реальных ключах запрещён.
Собственные цепочки отказов задача оборвала — чтение из хранилища и выгрузка в
Object Storage больше не называют ключ, — но текст, пришедший **чужими руками**,
идёт в журнал и в задачу как есть.
## Вопрос
Несёт ли текст отказа операции распознавания адрес объекта, который мы отдали
SpeechKit, и в каких случаях? Если несёт — чем его отсечь, не потеряв причину
отказа: разбором по образцу, перечнем известных кодов отказа или отказом писать
чужой текст вовсе.
## Куда ляжет ответ
- Наблюдение с командой и условиями — записка в `docs/research/`, рядом с
`pocketbase-defaults.md`.
- Если адрес приходит — норма в спеку `storage`, требование «Файл отдаётся
ссылкой», и строка в `docs/security.md`, раздел «Из чего строятся пути и
ключи»; сама правка — отдельной задачей.
- Если не приходит — строка в ту же записку, чтобы гипотезу не поднимали заново.
## Рамки
Реальные ключи Yandex и боевые данные не трогаем. Отказ придётся вызывать
дешёвым способом: битый объект, объект вне бакета, отозванный доступ — что из
этого доступно, решается по ходу. Цена прогона в деньгах называется до запуска.
+44
View File
@@ -0,0 +1,44 @@
# 🧹 Разобрать мелочи слоя хранилища
- **Тип:** chore
- **Категория:** Очередь
- **Зачем:** Три мелочи ниже потолка триажа: цикл воркера пишет потерю захвата уровнем ERROR и считает её отказом, тип ошибки заведён там, где конвенция просит sentinel, а FileName несёт два разных смысла.
Все три пришли ревью задачи `pocketbase-storage` 2026-08-12 и остались ниже
потолка отчёта — то есть реальны, но не блокировали. Собраны одной задачей,
потому что живут в одном пакете и правятся одним заходом.
1. **Потеря захвата — не отказ шага.** Значение «задачу за время работы забрал
другой» доезжает до цикла воркера, который пишет его уровнем `ERROR` и
увеличивает счётчик отказов. `docs/conventions/logging.md` относит «задача
досталась повторно» к `WARN`, а метрика отказов после этого считает не отказы.
2. **Тип ошибки против sentinel.** `docs/conventions/errors.md` просит sentinel
там, где данных у ошибки не читает никто; у значения о потерянном захвате
вызывающий читает только сам факт.
3. **Одно поле в двух смыслах.** У местной копии `entity.File.FileName` — имя в
хранилище и последняя часть ссылки на скачивание, у внешней — ключ объекта.
Различает их только соседнее поле, а совпадают они сегодня дословно: на этом
совпадении держится верность строки `docs/architecture.md` про ключ объекта.
## Затрагивает
- `internal/contract/error.go` — значение о потерянном захвате;
- цикл воркера `internal/controller/worker` — уровень записи и счётчик отказов;
- `internal/entity/file.go` и его потребители в `internal/service`;
- строка про ключ объекта в `docs/architecture.md`, «Внешние границы и форматы».
## Критерии приёмки
- Потеря захвата не считается отказом и не пишется на уровне владельца сервиса.
Оракул — тест воркера: после прогона с потерянным захватом счётчик отказов не
вырос, а строка в журнале идёт уровнем `WARN`.
- Форма значения об отказе совпадает с конвенцией. Оракул — `golangci-lint run`
плюс чтение `docs/conventions/errors.md` рядом с кодом.
- Два смысла имени файла разведены либо названы в документе. Оракул — `grep` по
потребителям поля: каждый читает его в одном смысле, а `docs/architecture.md`
говорит про ключ объекта то же, что делает код.
## Рамки
Раскладку файлов на диске и имена коллекций не трогаем: формат хранения объявлен
необратимым, а шаг схемы, уехавший на сервер, не переписывается.
+1
View File
@@ -3,6 +3,7 @@
- **Тип:** goal
- **Секция:** Направления
- **Зачем:** Расшифровка часового разговора — это стена текста: найти в списке нужную запись и вспомнить, о чём она, сегодня нечем.
- **Теги:** decomposed
Текст записи выдаётся уровнями: сырая расшифровка, вычитанный литературный
текст, заголовок в одну строку, темы-теги и пересказ в два-три предложения.
+1
View File
@@ -3,6 +3,7 @@
- **Тип:** goal
- **Секция:** Запланировано
- **Зачем:** Приём рассчитан на голосовое в пару мегабайт: обрыв на середине гигабайтного файла начинает загрузку заново, а один и тот же файл распознаётся повторно за наши деньги.
- **Теги:** decomposed
Человек отдаёт диктофонную запись или видео из семейного архива с телефона, по
сотовой сети, и загрузка либо доходит, либо честно говорит, что не дошла.
+1
View File
@@ -3,6 +3,7 @@
- **Тип:** goal
- **Секция:** Сопровождение
- **Зачем:** Распознавание и языковая модель оплачиваются по факту, а счёт приходит одной суммой: кто её набрал, из сервиса не выясняется.
- **Теги:** decomposed
Владелец открывает страницу и видит по каждому пользователю объём загруженного,
длительность записей в минутах и расход на внешние сервисы. Приглашая человека,
+1
View File
@@ -3,6 +3,7 @@
- **Тип:** goal
- **Секция:** Запланировано
- **Зачем:** Уровни текста считает платная модель, а уведомления приходят одним общим способом: отказаться от лишнего и выбрать свой канал пользователю нечем.
- **Теги:** decomposed
У каждого своя мера: одному нужна только сырая расшифровка, другому — все пять
уровней текста. Настройки принадлежат человеку, а не общему конфигу сервиса, и
+1
View File
@@ -3,6 +3,7 @@
- **Тип:** goal
- **Секция:** Запланировано
- **Зачем:** Сегодня записи принимает только бот и голый HTTP API без интерфейса: отдать сервис человеку, у которого нет Telegram, нечем.
- **Теги:** decomposed
Приложение получает поверхность, на которой запись загружают и забирают текст,
не открывая Telegram и не вызывая API руками. Ставится оно на телефон и