Нарезка задач под целевое состояние: PWA, вход, хранилище

Роадмап: web-access переименована под приложение, которое ставится на
телефон; заведена цель ready-notification — уведомление о готовности
без открытого приложения.

Беклог: десять задач. Многопользовательская цепочка (oidc-login,
record-ownership, telegram-account-link), веб (json-api-for-spa,
spa-skeleton, upload-and-status-screen, records-list-screen,
installable-pwa), уведомления через apprise и ntfy, разведка выбора
фреймворка. Очередь: долги, хранилище, вход, приложение.

Решение сменилось с htmx на SPA, поэтому conventions/web-ui.md снята
целиком и оставлена честной строкой до итога разведки. Открытые вопросы
архитектуры, границы паспорта и триггеры метки ревью приведены в
соответствие.
This commit is contained in:
av
2026-08-10 21:36:54 +03:00
parent 4d1c2bf44c
commit 115b3796e8
19 changed files with 536 additions and 160 deletions
+11 -1
View File
@@ -17,11 +17,21 @@
## Ядро
- [🐞 Починить тесты http-обработчика, ни разу не бывшие зелёными](items/http-handler-tests-never-green.md) — go test ./... падает на master: тесты требуют файла, которого нет в репозитории, и ждут 201 от ffprobe, которому скормили строку.
- [✨ Пускать в приложение только после входа через OIDC](items/oidc-login.md) — HTTP API открыт наружу без аутентификации: любой из интернета заводит задачи за наши деньги и читает чужие расшифровки по идентификатору.
- [✨ Привязать запись к владельцу и отдавать только свои](items/record-ownership.md) — У задачи и файла нет владельца, поэтому знание UUID задачи и есть право её читать.
- [✨ Сопоставить пользователя Telegram с учётной записью](items/telegram-account-link.md) — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны.
- [✨ Свести приём и чтение записей к одному контракту для приложения](items/json-api-for-spa.md) — Сегодняшний API отвечает 404 на любую ошибку чтения и 500 на любую ошибку приёма: строить на нём экраны нечем.
- [✨ Сделать экран загрузки записи и её состояния](items/upload-and-status-screen.md) — Первое, ради чего приложение открывают: отдать файл и увидеть, что с ним происходит.
- [✨ Сделать экран списка своих записей и чтения текста](items/records-list-screen.md) — Расшифровка сегодня доходит одним сообщением и теряется в переписке; вернуться к ней через неделю нечем.
- [✨ Отправлять готовый текст через apprise и ntfy](items/ntfy-delivery.md) — Пользователь веба узнаёт о готовности только опросом с открытого экрана.
- [🧹 Сравнивать доменные ошибки через errors.As](items/errors-as-instead-of-typecast.md) — NoopJobError и JobNotFoundError проверяются приведением типа: первая же обёртка %w между слоями сломает проверку молча.
- [🔬 Потолки SpeechKit по длине записи и по формату](items/speechkit-limits.md) — Потолок длины записи и перечень принимаемых форматов неизвестны, а цель про долгие записи без них не начинается.
- [🐞 Починить тесты http-обработчика, ни разу не бывшие зелёными](items/http-handler-tests-never-green.md) — go test ./... падает на master: тесты требуют файла, которого нет в репозитории, и ждут 201 от ffprobe, которому скормили строку.
## Инфра
- [🧹 Перевести хранилище на встроенный PocketBase](items/pocketbase-storage.md) — Хранилище, учётные записи и веб-панель нужны все три, и SQLite с goqu не даёт ни второго, ни третьего.
- [🔬 Выбор фреймворка для приложения](items/spa-framework-choice.md) — Конвенция веб-UI написана под htmx, а решено делать SPA: до выбора фреймворка не заводится ни сборка, ни первый экран.
- [✨ Собрать каркас приложения и раздать его из бинарника](items/spa-skeleton.md) — Экранов нет и собирать их нечем: ни сборки фронтенда, ни раздачи статики в проекте не существует.
- [✨ Сделать приложение устанавливаемым на телефон](items/installable-pwa.md) — Приложение, живущее вкладкой браузера, теряется среди прочих: ярлыка на экране у него нет.
- [🧹 Задать таймауты обращениям к внешним сервисам](items/external-call-timeouts.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата.
+2 -1
View File
@@ -22,8 +22,9 @@
## Запланировано
- [🎯 Записи загружаются и читаются в браузере](items/web-access.md) — Сегодня записи принимает только бот и голый HTTP API без интерфейса: отдать сервис человеку, у которого нет Telegram, нечем.
- [🎯 Записи загружаются и читаются в приложении, которое ставится на телефон](items/web-access.md) — Сегодня записи принимает только бот и голый HTTP API без интерфейса: отдать сервис человеку, у которого нет Telegram, нечем.
- [🎯 Сервисом пользуются несколько человек, и записи одного не видны другому](items/multi-user.md) — У задачи нет владельца, а HTTP API открыт наружу без аутентификации: пригласить второго человека сейчас значит открыть ему чужие расшифровки.
- [🎯 Пользователь узнаёт о готовности текста, не держа приложение открытым](items/ready-notification.md) — Расшифровка занимает минуты, и всё это время человек либо смотрит на экран с опросом статуса, либо забывает вернуться.
## Направления
+45
View File
@@ -0,0 +1,45 @@
# ✨ Сделать приложение устанавливаемым на телефон
- **Тип:** feature
- **Категория:** Инфра
- **Зачем:** Приложение, живущее вкладкой браузера, теряется среди прочих: ярлыка на экране у него нет.
- **Теги:** goal:web-access
Двигает пункты 5 и 6 «Завершения» цели: приложение ставится с телефона и
запускается с ярлыка без адресной строки, а открытое без сети показывает это
состоянием.
Берётся после того, как есть что ставить, — то есть после
`records-list-screen`.
## Затрагивает
- манифест приложения: имя, иконки под размеры экранов, цвета, режим показа;
- service worker: регистрация, обновление версии, поведение без сети;
- раздача манифеста и service worker с правильными заголовками и без длинного
кэша;
- версионирование собранной статики, чтобы установленное приложение обновлялось;
- `docs/conventions/web-ui.md` — раздел про статику и кэш;
- `docs/security.md` — service worker перехватывает запросы, и это новая
поверхность.
## Критерии приёмки
- Приложение предлагается к установке и после установки запускается с ярлыка на
отдельном экране, без адресной строки. Оракул — проверка на телефоне и
сверка манифеста инструментом браузера.
- Открытое без сети приложение показывает состояние «связи нет», а не пустую
страницу и не ошибку браузера. Оракул — тест либо ручная проверка с
выключенной сетью.
- Новая выкладка доходит до установленного приложения: старая статика из кэша не
переживает обновление. Оракул — установка, выкладка изменённой версии,
перезапуск приложения показывает новую.
- Service worker не кэширует ответы API и не отдаёт чужие данные после смены
сессии. Оракул — тест: после выхода и входа другой учётной записью список
записей чужих не содержит.
## Рамки
Офлайн-чтения готовых расшифровок и очереди отправки без сети **не делаем**
это за границей цели. Web Push не делаем: уведомления идут через apprise и ntfy,
цель `ready-notification`.
+44
View File
@@ -0,0 +1,44 @@
# ✨ Свести приём и чтение записей к одному контракту для приложения
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** Сегодняшний API отвечает 404 на любую ошибку чтения и 500 на любую ошибку приёма: строить на нём экраны нечем.
- **Теги:** goal:web-access
Двигает пункты 1, 2 и 4 «Завершения» цели: экраны заводят задачу, видят её
состояние и листают список — всё через один контракт.
Обработчик `GET /api/status/:id` сегодня отвечает `404` на **любую** ошибку
чтения, включая сбой базы, а `POST /api/audio``500` на любую ошибку заведения,
включая негодный файл. Экран, построенный на таком контракте, показывает «не
найдено» при упавшей базе.
## Затрагивает
- `POST /api/audio` и `GET /api/status/:id` — коды ответа и форма ошибки;
- новый эндпоинт списка своих записей с постраничным чтением;
- единая точка отображения доменной ошибки в код ответа — её сегодня нет
([conventions/errors.md](../../docs/conventions/errors.md));
- `internal/contract` — доменные ошибки под отображение;
- `internal/controller/http` целиком;
- `docs/architecture.md`, раздел «Единые точки проекта».
## Критерии приёмки
- Сбой базы при чтении задачи даёт `500`, а не `404`. Оракул — тест с
репозиторием, возвращающим ошибку драйвера.
- Негодный файл даёт `400` с человекочитаемым текстом, а не `500`. Оракул —
тест: файл, который отвергает разбор метаданных.
- Тело ошибки одной формы на всех эндпоинтах и не содержит сырого `err.Error()`.
Оракул — тест на четырёх ветвях отказа: форма совпадает, текста внутренней
ошибки в теле нет.
- Список записей отдаётся страницами и упорядочен по времени создания. Оракул —
тест на выборке больше страницы.
- Отображение ошибки живёт в одной функции, и она названа в
`docs/architecture.md`. Оракул — `task gate`, шаг `docs.py check`.
## Рамки
Аутентификацию и владельца не заводим — это `oidc-login` и `record-ownership`;
задача про форму контракта. Публичный контракт после мерджа обратной правкой не
откатывается.
+46
View File
@@ -0,0 +1,46 @@
# ✨ Отправлять готовый текст через apprise и ntfy
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** Пользователь веба узнаёт о готовности только опросом с открытого экрана.
- **Теги:** goal:ready-notification
Двигает все пять пунктов «Завершения» цели: готовый текст и отказ доходят до
пользователя веба без открытого приложения, адрес канала свой у каждого, а отказ
канала задачу не роняет.
Сегодня `completeJob` и `failJob` отвечают только источнику `telegram`;
источник `api` не получает ничего. Здесь появляется второй способ доставки, и
выбор между ними обязан остаться в одной точке — той же, что сейчас.
## Затрагивает
- `internal/contract` — интерфейс отправителя уведомления;
- `internal/service`, `completeJob` и `failJob` — выбор канала по источнику
задачи;
- новый адаптер поверх apprise либо прямого HTTP к ntfy;
- адрес канала у учётной записи: колонка и её миграция, экран настройки;
- секция конфигурации: адрес сервера ntfy, способ вызова apprise;
- `docs/architecture.md` — новая внешняя зависимость и чем она отказывает;
- `docs/security.md` — текст расшифровки уходит на внешний сервис;
- `docs/database.md` — настройки числами.
## Критерии приёмки
- Задача из веба, дошедшая до `done`, отправляет текст в канал владельца.
Оракул — тест с подставным отправителем: вызов ровно один, с текстом задачи и
адресом владельца.
- Задача из Telegram по-прежнему отвечает ботом и вторым каналом не дублируется.
Оракул — тот же тест на источнике `telegram`: подставной отправитель ntfy не
вызван.
- Недоступный канал уведомлений не мешает задаче завершиться: состояние `done` и
текст в приложении остаются. Оракул — тест с отправителем, возвращающим
ошибку: задача в `done`, текст на месте, в логе одна запись уровня `WARN`.
- Отказ задачи доходит тем же каналом и тем же человекочитаемым текстом, что
видит пользователь Telegram. Оракул — тест на ветке `failJob`.
## Рамки
Web Push с VAPID и своим хранением подписок не делаем. Своего сервера ntfy не
поднимаем — адрес приходит конфигом. Текст расшифровки уходит на внешний сервис,
и это сдвиг периметра: строка в `docs/security.md` обязательна.
+50
View File
@@ -0,0 +1,50 @@
# ✨ Пускать в приложение только после входа через OIDC
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** HTTP API открыт наружу без аутентификации: любой из интернета заводит задачи за наши деньги и читает чужие расшифровки по идентификатору.
- **Теги:** goal:multi-user, question
Двигает пункт 1 «Завершения» цели: неаутентифицированный запрос к записям не
проходит ни к странице, ни к API.
Провайдер — Authelia по OIDC. Своей регистрации и своих паролей не делаем, это
граница из [паспорта](../../docs/passport.md). Разграничения записей по владельцу
здесь ещё нет: после входа видно всё, что видно сейчас, — этим занимается
`record-ownership`.
## Затрагивает
- новые эндпоинты входа и выхода, обработка ответа провайдера;
- `POST /api/audio` и `GET /api/status/:id` — оба уходят за аутентификацию;
- `GET /health` и `GET /metrics` — решить и записать, остаются ли открытыми;
- хранение сессии: таблица или подписанная кука;
- секция конфигурации под провайдера: адрес, идентификатор клиента, секрет;
- `docs/security.md` — периметр меняется, и первая его строка перестаёт быть
верной;
- `config.dist.toml` и `internal/config`.
## Критерии приёмки
- Запрос к `POST /api/audio` и `GET /api/status/:id` без сессии получает отказ, а
не заводит задачу и не отдаёт текст. Оракул — тест на обоих эндпоинтах без
куки: код ответа 401 либо 302 на вход, тело без данных задачи.
- Сессия переживает перезапуск приложения. Оракул — тест: запрос с прежней кукой
после пересоздания сервера проходит.
- Выход из сессии закрывает доступ. Оракул — тест: после выхода тот же запрос
получает отказ.
- Секрет провайдера не попадает ни в лог, ни в ответ. Оракул — тест на отсутствие
значения секрета в записанном выводе логгера.
- Первая строка `docs/security.md` описывает новый периметр. Оракул — `task
gate`, шаг `docs.py check`.
## Рамки
Владельца у записи здесь не заводим и выборку не сужаем: после входа видно
столько же, сколько сейчас. Инвариант «бот отвечает только тем, кто в белом
списке» не трогаем — он живёт до `telegram-account-link`.
## Вопросы
Остаются ли `GET /health` и `GET /metrics` открытыми? Прокси и система сбора
метрик сессии не имеют, но и наружу их отдавать незачем.
+25
View File
@@ -0,0 +1,25 @@
# 🎯 Пользователь узнаёт о готовности текста, не держа приложение открытым
- **Тип:** goal
- **Секция:** Запланировано
- **Зачем:** Расшифровка занимает минуты, и всё это время человек либо смотрит на экран с опросом статуса, либо забывает вернуться.
Задача уходит в работу — человек закрывает приложение и получает сообщение,
когда текст готов. Пользователь Telegram это уже имеет: бот отвечает сам.
Пользователь веба — нет.
Доставку берём внешнюю: apprise как отправитель, ntfy как канал. Web Push с
VAPID и собственным хранением подписок за целью **не стоит** — это отдельная
инфраструктура ради того же результата.
## Завершение
1. Готовый текст доходит до пользователя веба сообщением, без открытого
приложения.
2. Отказ задачи доходит тем же путём и тем же человекочитаемым текстом, что
видит пользователь Telegram.
3. Адрес канала уведомлений задаёт пользователь, а не общий конфиг: у каждого
свой.
4. Недоступность канала уведомлений не роняет задачу и не мешает ей завершиться:
текст остаётся в приложении.
5. Пользователь Telegram получает ответ по-прежнему ботом, а не вторым каналом.
+38
View File
@@ -0,0 +1,38 @@
# ✨ Привязать запись к владельцу и отдавать только свои
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** У задачи и файла нет владельца, поэтому знание UUID задачи и есть право её читать.
- **Теги:** goal:multi-user
Двигает пункт 2 «Завершения» цели: выборка чужой записи по её идентификатору
возвращает «не найдено», а не содержимое.
Берётся после `oidc-login`: до входа неизвестно, кто владелец.
## Затрагивает
- таблица задач и таблица файлов — колонка владельца и её миграция;
- `internal/contract`, интерфейсы репозиториев: чтение сужается владельцем;
- `internal/adapter/repo/sqlite` — все четыре запроса задач;
- `internal/service`, оба метода заведения задачи;
- `GET /api/status/:id` — ответ на чужую запись;
- `docs/database.md` — схема и правило выборки.
## Критерии приёмки
- Запрос чужой записи по её идентификатору возвращает «не найдено», а не
содержимое и не «доступ запрещён». Оракул — тест: две сессии, задача первой
запрашивается второй, ответ 404 и пустое тело.
- Запись, заведённая из веба, принадлежит вошедшему; заведённая ботом —
пользователю, за которым закреплён чат. Оракул — тест на оба входа со сверкой
колонки владельца.
- Выборка воркера владельцем **не** сужается: конвейер обрабатывает записи всех.
Оракул — тест: задачи двух владельцев проходят конвейер одним воркером.
- База заводится с чистого листа, колонка владельца обязательна и без умолчания.
Оракул — прогон миграций на пустой базе и попытка вставки без владельца.
## Рамки
Совместного доступа, ролей и передачи записи другому не делаем: владелец один и
неизменяем. Данные не переносим — чистый лист.
+37
View File
@@ -0,0 +1,37 @@
# ✨ Сделать экран списка своих записей и чтения текста
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** Расшифровка сегодня доходит одним сообщением и теряется в переписке; вернуться к ней через неделю нечем.
- **Теги:** goal:web-access
Двигает пункты 3 и 4 «Завершения» цели: список своих записей открывается и
листается, а готовый текст читается и копируется целиком.
Берётся после `upload-and-status-screen`.
## Затрагивает
- новый экран списка и экран одной записи;
- постраничное чтение из контракта API;
- копирование текста в буфер обмена;
- показ записи, которая ещё в работе, — тем же состоянием, что на экране
загрузки.
## Критерии приёмки
- Список показывает записи вошедшего, новые сверху, и листается дальше первой
страницы. Оракул — тест экрана на подставном API с двумя страницами.
- Текст готовой записи открывается целиком, без деления на части. Оракул — тест
на записи с текстом длиннее предела сообщения Telegram: на экране весь текст.
- Текст копируется одним действием. Оракул — тест: после действия в буфере обмена
тот же текст.
- Запись в работе показывается состоянием и доходит до текста без перезагрузки
экрана. Оракул — тест: подставной API отвечает `transcribe`, затем `done`;
экран переходит к тексту сам.
## Рамки
Поиска по тексту, переименования и удаления записей не делаем. Правки текста не
делаем — это граница из [паспорта](../../docs/passport.md): расшифровку отдаём как
есть.
+34
View File
@@ -0,0 +1,34 @@
# 🔬 Выбор фреймворка для приложения
- **Тип:** research
- **Категория:** Инфра
- **Зачем:** Конвенция веб-UI написана под htmx, а решено делать SPA: до выбора фреймворка не заводится ни сборка, ни первый экран.
- **Теги:** goal:web-access
Конвенция [web-ui.md](../../docs/conventions/web-ui.md) описывала htmx с прямым
запретом на шаг сборки и реактивные фреймворки. Решение сменилось на SPA, и
теперь эта конвенция ждёт переписывания — но написать её не под что, пока
фреймворк не назван.
Разведка, а не задача: исход — знание и переписанная конвенция, а не работающий
экран.
## Вопрос
Какой фреймворк берём под приложение на четыре экрана, которое собирается в
статику, вшивается в бинарник Go и ставится на телефон как PWA.
## Куда ляжет ответ
`docs/research/spa-framework.md` — сравнение с числами: размер собранной статики,
время холодной сборки, число зависимостей. Выбор и причина отказа от остальных —
в `docs/adr/`, потому что переделка на другой фреймворк стоит дороже переписывания
одного файла. Следом переписывается `docs/conventions/web-ui.md`, и до тех пор
она стоит честной строкой «фреймворк не выбран».
## Рамки
Сравнение идёт на одном и том же пробном экране: список записей с опросом
состояния. Кандидаты названы человеком; сегодня в разговоре звучали Svelte, Vue
и React, все с Vite. Мера — размер статики, простота вшивания в `go:embed` и
цена шага сборки в гейте, а не популярность.
+43
View File
@@ -0,0 +1,43 @@
# ✨ Собрать каркас приложения и раздать его из бинарника
- **Тип:** feature
- **Категория:** Инфра
- **Зачем:** Экранов нет и собирать их нечем: ни сборки фронтенда, ни раздачи статики в проекте не существует.
- **Теги:** goal:web-access
Двигает «Завершение» цели опосредованно: сам по себе каркас пользователю ничего
не даёт, но без него ни один экран не соберётся. Видимая польза — приложение
открывается и показывает, что оно живо и кто вошёл.
Берётся после `spa-framework-choice`: собирать не на чем, пока фреймворк не
выбран.
## Затрагивает
- новый каталог фронтенда: исходники, зависимости, конфигурация сборки;
- `Taskfile.yml` — шаг сборки статики и его место в `task gate` и `task image`;
- `Dockerfile` — сборка статики внутри образа, чтобы выкладка не зависела от
машины разработчика;
- раздача статики из бинарника через `go:embed` и роут под неё;
- `.gitignore` — каталог зависимостей и собранная статика;
- `docs/conventions/web-ui.md` — переписывается под выбранный фреймворк;
- `docs/architecture.md` — новый компонент и новая внешняя зависимость сборки.
## Критерии приёмки
- Статика собирается одной командой и вшивается в бинарник: запущенный бинарник
отдаёт приложение без каталога рядом. Оракул — сборка, запуск бинарника из
пустого каталога, запрос корня возвращает разметку приложения.
- Образ собирается на чистой машине без предустановленного окружения фронтенда.
Оракул — `task image` без локально установленных зависимостей фронтенда.
- Приложение показывает, кто вошёл, и обращается к API живого сервера. Оракул —
тест либо ручная проверка на запущенном сервере с сессией.
- Шаг сборки статики входит в `task gate` и краснеет при ошибке сборки. Оракул —
намеренно сломанный исходник роняет `task gate`.
## Рамки
Экранов, кроме заглушки с именем вошедшего, не делаем — это `upload-and-status-screen`
и `records-list-screen`. Установку на телефон не делаем — это `installable-pwa`.
Появление шага сборки в гейте меняет требования к машине разработчика: это
решение уровня ADR, и оно записывается.
+43
View File
@@ -0,0 +1,43 @@
# ✨ Сопоставить пользователя Telegram с учётной записью
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны.
- **Теги:** goal:multi-user
Двигает пункты 4 и 5 «Завершения» цели: записи из бота видны тому же человеку в
приложении, а белый список перестаёт быть отдельным механизмом — право писать
боту выводится из учётной записи.
Берётся после `record-ownership`: связывать не с чем, пока у записи нет
владельца.
## Затрагивает
- связь учётной записи с чатом Telegram: таблица и её миграция;
- команда бота для привязки и способ её подтвердить;
- `internal/controller/tg`, проверка отправителя — сегодня `slices.Contains` по
строке автора;
- ключ конфигурации `[server] users_while_list` — он уходит;
- `config.dist.toml`, `internal/config`;
- `docs/security.md`, раздел «Что разграничивает доступ»;
- инвариант «Бот отвечает только тем, кто в белом списке» в `CLAUDE.md`.
## Критерии приёмки
- Сообщение от непривязанного чата обрабатывать не начинаем и файл не скачиваем.
Оракул — тест: обновление от неизвестного чата не заводит задачу и не ходит в
сеть.
- Привязка идёт по идентификатору чата, а не по имени пользователя: смена имени
в Telegram доступа не меняет. Оракул — тест: то же обновление с изменённым
`From.UserName` по-прежнему принимается.
- Запись, пришедшая ботом, видна тому же человеку в приложении. Оракул — тест:
задача из привязанного чата читается сессией связанной учётной записи и не
читается чужой.
- Ключ `users_while_list` из конфига и из документов убран. Оракул — `task
gate`, шаг `docs.py check`, и `grep users_while_list` по репозиторию пуст.
## Рамки
Одна учётная запись — один чат: нескольких чатов на человека не заводим.
Инвариант в `CLAUDE.md` переписывается этой же задачей, а не остаётся врать.
+38
View File
@@ -0,0 +1,38 @@
# ✨ Сделать экран загрузки записи и её состояния
- **Тип:** feature
- **Категория:** Ядро
- **Зачем:** Первое, ради чего приложение открывают: отдать файл и увидеть, что с ним происходит.
- **Теги:** goal:web-access
Двигает пункты 1 и 2 «Завершения» цели: экран принимает файл и заводит задачу, а
её состояние обновляется само, пока задача не дошла до `done` или `failed`.
Берётся после `spa-skeleton` и `json-api-for-spa`.
## Затрагивает
- новый экран приложения: выбор файла, отправка, показ состояния;
- опрос состояния задачи и его остановка;
- показ отказа человекочитаемым текстом из контракта API;
- предел размера загружаемого файла на стороне приложения и на стороне сервера
(`router.MaxMultipartMemory`, сегодня 32 МиБ);
- `docs/database.md` — частота опроса числом.
## Критерии приёмки
- Выбранный файл уходит на сервер и заводит задачу; экран сразу показывает её
состояние. Оракул — тест экрана на подставном API: после отправки на экране
идентификатор задачи и состояние `created`.
- Опрос сам прекращается, когда задача дошла до `done` или `failed`. Оракул —
тест: после ответа `done` новых запросов к API нет.
- Отказ задачи показывается человекочитаемым текстом, а не кодом и не сырой
ошибкой. Оракул — тест на ответе с состоянием `failed`.
- Файл больше предела отклоняется на экране до отправки, с названным числом.
Оракул — тест на файле сверх предела: запроса к API нет, на экране предел
числом.
## Рамки
Записи звука в приложении не делаем — файл выбирают в системном диалоге. Список
прошлых записей не делаем, это `records-list-screen`.
+16 -9
View File
@@ -1,19 +1,26 @@
# 🎯 Записи загружаются и читаются в браузере
# 🎯 Записи загружаются и читаются в приложении, которое ставится на телефон
- **Тип:** goal
- **Секция:** Запланировано
- **Зачем:** Сегодня записи принимает только бот и голый HTTP API без интерфейса: отдать сервис человеку, у которого нет Telegram, нечем.
Приложение получает поверхность, на которой запись загружают и забирают текст,
не открывая Telegram и не вызывая API руками. Конвейер обработки при этом
остаётся прежним — меняется только вход и способ показать результат.
не открывая Telegram и не вызывая API руками. Ставится оно на телефон и
открывается с ярлыка, как обычное приложение. Конвейер обработки при этом
остаётся прежним — меняется вход и способ показать результат.
Границы взяты уже: запись звука в самом приложении и работа без сети за целью
**не стоят** — файл выбирают в системном диалоге, а без сети приложение
показывает, что связи нет.
## Завершение
1. Страница принимает файл формой и заводит задачу — ту же, что заводит бот.
2. Состояние задачи видно на странице и обновляется само, пока задача не дошла
до `done` или `failed`; отказ показывается человекочитаемым текстом.
3. Готовый текст читается и копируется со страницы целиком, без деления на
части.
1. Экран принимает файл и заводит задачу — ту же, что заводит бот.
2. Состояние задачи видно на экране и обновляется само, пока задача не дошла до
`done` или `failed`; отказ показывается человекочитаемым текстом.
3. Готовый текст читается и копируется с экрана целиком, без деления на части.
4. Список своих записей открывается и листается.
5. Всё перечисленное работает без JavaScript — формой и переходом по ссылке.
5. Приложение ставится на телефон из браузера и запускается с ярлыка на
отдельном экране, без адресной строки.
6. Открытое без сети, приложение показывает это состоянием, а не пустой
страницей и не ошибкой браузера.