Нарезка задач под целевое состояние: 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:
+11
-2
@@ -120,8 +120,17 @@
|
||||
другим. Данные не переносим — начинаем с чистого листа.
|
||||
- **Учётные записи.** Вход через OIDC, провайдер — Authelia. Не решено, где
|
||||
живёт сессия и как связываются пользователь Telegram и пользователь веба.
|
||||
- **Веб-интерфейс.** Формы нет вовсе, есть только API. Конвенция веб-UI на htmx
|
||||
описана в [conventions/web-ui.md](conventions/web-ui.md) заранее.
|
||||
- **Приложение.** Экранов нет вовсе, есть только API. Решено делать SPA,
|
||||
устанавливаемое на телефон; фреймворк выбирает разведка
|
||||
`spa-framework-choice`, и до её итога
|
||||
[conventions/web-ui.md](conventions/web-ui.md) стоит почти пустой. Шаг сборки
|
||||
фронтенда меняет требования к машине разработчика и к образу — решение уровня
|
||||
ADR.
|
||||
- **Уведомления.** Пользователь веба узнаёт о готовности только опросом.
|
||||
Доставку решено брать внешнюю — apprise как отправитель, ntfy как канал; Web
|
||||
Push с VAPID отвергнут. Появляется внешняя зависимость, которой сегодня нет, и
|
||||
текст расшифровки начинает уходить на сторону — сдвиг периметра
|
||||
[security.md](security.md).
|
||||
- **Долгие записи.** Потолок сегодня неизвестен и не замерялся: 20 МиБ на приём
|
||||
из Telegram — точно, ограничения `deferred-general` по длине — нет.
|
||||
- **Формат для распознавания.** Конвертер отдаёт ogg/vorbis (`libvorbis`), а
|
||||
|
||||
@@ -15,11 +15,14 @@ severity — в [CLAUDE.md](../../CLAUDE.md).
|
||||
|
||||
## Откуда взяты и что с расхождениями
|
||||
|
||||
Все пять записей перенесены из проекта jellybit — тот же Go, тот же автор, те же
|
||||
Четыре записи перенесены из проекта jellybit — тот же Go, тот же автор, те же
|
||||
задачи. Код transcriber написан раньше и **части правил не следует**: ключи —
|
||||
UUID вместо ULID, время берётся `time.Now()` по месту, лог пишется на каждом
|
||||
шаге и дублируется воркером, доменные ошибки проверяются приведением типа.
|
||||
|
||||
Пятая, `web-ui.md`, тоже пришла оттуда, но не прижилась: jellybit работает на
|
||||
htmx, а здесь решено делать SPA — и перенесённый текст снят целиком.
|
||||
|
||||
Каждое такое место названо в своей записи строкой «*Расхождение:*». Читается оно
|
||||
как **долг, а не как нарушение**: правила действуют на новый код, переписывание
|
||||
существующего — отдельная работа. Проходу ревью строка «Расхождение» говорит, что
|
||||
@@ -36,10 +39,10 @@ UUID вместо ULID, время берётся `time.Now()` по месту,
|
||||
`0600`, самодокументируемый `config.dist.toml`, проверка на старте.
|
||||
- [database.md](database.md) — БД и идентификаторы: время в UTC RFC 3339, TEXT
|
||||
ULID, разбор на входной границе, естественные ключи у деталей.
|
||||
- [web-ui.md](web-ui.md) — веб-UI на htmx: партиал равен странице равен
|
||||
фрагменту, ветвление по `HX-Request`, деградация без JS, ошибка на htmx-пути
|
||||
как 200 плюс фрагмент, самозавершающийся опрос, вендоринг статики. **Записана
|
||||
наперёд: веб-UI ещё нет.**
|
||||
- [web-ui.md](web-ui.md) — веб-UI: что решено про приложение (SPA, установка на
|
||||
телефон, статика в бинарнике) и что ждёт выбора фреймворка. **Почти пуста:**
|
||||
редакция на htmx снята 2026-08-10 вместе со сменой решения, а новую писать не
|
||||
под что до разведки `spa-framework-choice`.
|
||||
|
||||
## Механизировано
|
||||
|
||||
|
||||
+33
-138
@@ -1,146 +1,41 @@
|
||||
# Веб-UI (htmx)
|
||||
# Веб-UI
|
||||
|
||||
Конвенция: *как* мы пишем код веб-UI — частичная замена фрагментов, опрос живых
|
||||
обновлений, обработчики действий, деградация без JS, ошибки. Это правила
|
||||
оформления кода (How), а не спецификация поведения — что именно UI показывает и
|
||||
какие действия обязан поддерживать, живёт в спеке OpenSpec.
|
||||
Конвенция: *как* мы пишем код приложения. Это правила оформления кода (How), а не
|
||||
спецификация поведения — что именно приложение показывает и какие действия
|
||||
обязано поддерживать, живёт в спеке OpenSpec.
|
||||
|
||||
**Взято из проекта jellybit и записано наперёд: веб-UI в transcriber нет вовсе.**
|
||||
Есть только HTTP API на gin. Ни одного расхождения назвать нельзя — нечему
|
||||
расходиться; правила действуют с первой страницы, которую заведём.
|
||||
**Фреймворк не выбран, и до выбора эта конвенция пуста.** Здесь стоит только то,
|
||||
что решено и от фреймворка не зависит; всё остальное появится по итогу разведки
|
||||
`spa-framework-choice`, которая же заведёт запись в `research/` и ADR.
|
||||
|
||||
Прежняя редакция описывала htmx с прямым запретом на шаг сборки и реактивные
|
||||
фреймворки. Решение сменилось на SPA 2026-08-10, и та редакция снята целиком:
|
||||
частичная подмена фрагментов, ветвление по `HX-Request` и деградация без JS к
|
||||
SPA не относятся ни одним пунктом.
|
||||
|
||||
Логирование запросов — [logging.md](logging.md). Трансляция доменных ошибок
|
||||
наружу — [errors.md](errors.md). Здесь — только особенности htmx-транспорта, без
|
||||
повторения.
|
||||
наружу — [errors.md](errors.md).
|
||||
|
||||
## Стек и границы
|
||||
## Что решено
|
||||
|
||||
htmx-first: `gin` плюс `html/template` (рендер на сервере) плюс htmx. Ничего
|
||||
сверх этого: **без шага сборки, без Node и сборщика, без реактивных
|
||||
фреймворков**. htmx вендорится и раздаётся с нашего же хоста (`go:embed`,
|
||||
`/static/vendor/`), без CDN.
|
||||
- **Приложение — SPA**, а не страницы, отрисованные сервером. Сервер отдаёт
|
||||
контракт данных, разметку собирает клиент.
|
||||
- **Приложение ставится на телефон** и запускается с ярлыка: манифест, иконки,
|
||||
service worker.
|
||||
- **Статика вшивается в бинарник** через `go:embed` и раздаётся им же. Внешнего
|
||||
веб-сервера под статику не заводим, бинарник остаётся самодостаточным.
|
||||
- **Шрифты и скрипты — со своего хоста**, без внешних. Внешних ресурсов времени
|
||||
выполнения нет.
|
||||
- **Офлайн-чтения расшифровок и очереди отправки без сети не делаем** — граница
|
||||
цели [web-access](../../tasks/items/web-access.md). Без сети приложение
|
||||
показывает состояние, а не пустой экран.
|
||||
- **Web Push не делаем**: уведомления идут через apprise и ntfy, цель
|
||||
[ready-notification](../../tasks/items/ready-notification.md).
|
||||
- **Записи звука в приложении не делаем** — файл выбирают системным диалогом.
|
||||
|
||||
- Свой JS сведён к минимуму: только то, чего серверу знать не нужно.
|
||||
**Клиентского пересчёта доменного состояния нет** — состояние считает сервер,
|
||||
клиент лишь подменяет присланную разметку.
|
||||
- Alpine.js и SPA сознательно **не вводим**. Понадобится реактивный клиентский
|
||||
виджет — вводим отдельным изменением и записываем решение, не раньше.
|
||||
## Что не решено
|
||||
|
||||
## Единый источник разметки: партиал равен странице равен фрагменту
|
||||
|
||||
Переиспользуемый кусок — это `{{define "name"}}` в каталоге партиалов. Тот же
|
||||
`{{define}}` рендерится **и** внутри страницы (`{{template "name" .}}`), **и**
|
||||
как ответ-фрагмент того же обработчика. Отдельной разметки под фрагмент не
|
||||
заводим — иначе она разъедется со страницей.
|
||||
|
||||
**Инвариант: корень `{{define}}` — это элемент с целевым `id`**, например
|
||||
`#job-{id}`. `hx-swap="outerHTML"` заменяет весь корневой узел; если ответный
|
||||
фрагмент не несёт тот же корневой `id`, следующее действие или опрос не найдёт
|
||||
цель. Разметку и `id` держим в одном партиале.
|
||||
|
||||
Сборку данных для шаблона выносим в отдельную функцию и зовём её и на полной
|
||||
странице, и во фрагменте — чтобы htmx-ветка не копировала сборку.
|
||||
|
||||
## Обработчик действия: ветвление htmx и редирект
|
||||
|
||||
htmx-запрос определяем по заголовку `HX-Request: true`.
|
||||
|
||||
Обработчик действия зовёт доменную операцию **одинаково** в обеих ветках, а
|
||||
дальше ветвится: без htmx — привычный редирект после POST (303); с htmx —
|
||||
перечитать актуальное состояние, собрать данные тем же сборщиком и отдать
|
||||
фрагмент.
|
||||
|
||||
Рендер именованного шаблона идёт **в буфер** и только затем пишется в ответ: при
|
||||
ошибке шаблона клиент не получит полстраницы.
|
||||
|
||||
## Деградация без JS обязательна
|
||||
|
||||
Формы действий остаются обычными `<form method="post" action="...">`, а
|
||||
`hx-post`, `hx-target` и `hx-swap` лишь **накладываются сверху** на ту же форму.
|
||||
Без JS всё работает через POST и редирект. Атрибут `action` — рабочий запасной
|
||||
путь, а не украшение.
|
||||
|
||||
Фильтр, поиск и разбиение списка на страницы — **серверные**, параметрами
|
||||
запроса, тоже без JS. Клиентской фильтрации нет намеренно.
|
||||
|
||||
## Ошибки на htmx-пути: HTTP 200 и фрагмент
|
||||
|
||||
htmx по умолчанию **не подменяет DOM на ответы 4xx и 5xx**. Поэтому при ошибке
|
||||
действия обработчик отвечает **200 с фрагментом**, несущим сообщение. Доменную
|
||||
ошибку на htmx-пути **не** транслируем в HTTP-статус — в отличие от API и от
|
||||
пути без JS.
|
||||
|
||||
- Сообщение — нейтральный текст публичного канала (см. [errors.md](errors.md));
|
||||
сырой `err.Error()` наружу не идёт.
|
||||
- Ошибку кладём в **отдельное поле** под ошибку действия, не перегружая доменные
|
||||
поля: непустое доменное поле перекрыло бы сообщение.
|
||||
- **При ошибке активное состояние не меняем** — перечитанные данные показывают
|
||||
прежний выбор плюс сообщение.
|
||||
|
||||
## Живой опрос
|
||||
|
||||
Приём живого обновления: эндпоинт фрагмента плюс в разметке `hx-get`,
|
||||
`hx-trigger="every Ns"` и `hx-swap="outerHTML"`. Прямой предмет опроса в
|
||||
transcriber — карточка задачи, пока та не дошла до `done` или `failed`.
|
||||
|
||||
- **Один опросчик на обновляемый корень.** Опрашивает себя корень поверхности, а
|
||||
вложенные живые области своего `hx-get` **не несут**: подмена корня уносит их
|
||||
вместе с таймером, и два опроса подменяли бы разметку друг друга.
|
||||
- **Опросчик самозавершается.** Опрос идёт, пока предмет может измениться без
|
||||
участия браузера; перестал — фрагмент возвращается **без `hx-*`**, и htmx
|
||||
больше не опрашивает. Для задачи это значит: `created`, `converted` и
|
||||
`transcribe` наблюдаемы, `done` и `failed` — нет.
|
||||
- **Отказ тика тоже самозавершается.** Не сумев прочитать задачу, тик отвечает
|
||||
`200` и фрагментом с объяснением **без `hx-*`**: htmx не подменяет DOM на
|
||||
`4xx` и `5xx`, поэтому статус ошибки оставил бы поверхность навсегда прежней, а
|
||||
опрос — бесконечным. Фрагмент отказа обязан нести корневой `id` того узла,
|
||||
который он собой заменяет.
|
||||
- **Уровень лога у тика — `WARN`.** У повторяющегося опроса есть штатный повтор;
|
||||
`ERROR` оставляем разовому действию человека (см. [logging.md](logging.md)).
|
||||
- **Подмена всего фрагмента через `outerHTML`** удаляет старый узел вместе с его
|
||||
опросчиком, и htmx заново размечает новый — двойного опроса нет **при условии
|
||||
совпадения корневого `id`**. Эфемерное состояние разметки подмену не
|
||||
переживает: то, что должно пережить тик (раскрытый `<details>`), помечается
|
||||
`hx-preserve`.
|
||||
- **Частота — по цене тика, и она называется числом** в таблице настроек
|
||||
[../database.md](../database.md) в тот же момент, когда заводится первый
|
||||
опрашиваемый экран; сегодня такой настройки нет. Опрашивать чаще, чем меняется
|
||||
источник, бессмысленно: задачу двигает воркер с шагом в секунду.
|
||||
- **Тик ходит в БД, и это цена решения.** Читать состояние задачи дешевле, чем
|
||||
держать снимок в памяти, но каждый открытый браузер добавляет запросов.
|
||||
|
||||
## Подмена сохраняет контекст; выход — навигация
|
||||
|
||||
`hx-swap="outerHTML"` не сбрасывает прокрутку и не трогает серверные фильтр,
|
||||
поиск и страницу (они в параметрах запроса). Действие **не должно уводить**
|
||||
пользователя со страницы, если предмет остаётся на ней.
|
||||
|
||||
Действие, после которого предмет **покидает** страницу (удаление записи),
|
||||
остаётся **обычной POST-формой без `hx-*`**, то есть полной навигацией. Признак
|
||||
«это выход» — форма без htmx-атрибутов; так не нужен `HX-Redirect`, а «уйти с
|
||||
экрана» выражено самой навигацией.
|
||||
|
||||
**Асинхронные действия.** Доменное действие асинхронно почти всегда: загрузка
|
||||
записи только заводит задачу, работу доделывают воркеры. Подмена отдаёт
|
||||
**промежуточное** состояние, а не мнимый результат; готовый итог догоняем
|
||||
самозавершающимся опросом. Мгновенный итог в UI не обещаем.
|
||||
|
||||
## Различение поверхности одного действия
|
||||
|
||||
Один и тот же роут действия, вызванный с разных страниц, отдаёт разные фрагменты.
|
||||
Различаем **явным скрытым полем формы** `surface=list|detail`, а не догадкой по
|
||||
`HX-Target` или `Referer`: поле самодокументируемо и не зависит от разрешения
|
||||
цели.
|
||||
|
||||
## Статика, вендоринг, кэш
|
||||
|
||||
- Ресурсы встроены через `go:embed`, отдаются под `/static/` с длинным
|
||||
неизменяемым кэшем (`Cache-Control: public, max-age=31536000, immutable`).
|
||||
- Меняемые ресурсы (css, js) версионируются параметром `?v=<версия>` — коротким
|
||||
sha256 их содержимого, URL строит помощник шаблона. Свежая выкладка не отдаёт
|
||||
устаревший файл.
|
||||
- Вендор (htmx, шрифты) адресуется по **неизменному имени файла**, и параметр
|
||||
версии ему не нужен. В git его **не коммитим**; задача сборки идемпотентно
|
||||
добывает его по манифесту со сверкой sha256.
|
||||
- Шрифты и скрипты — **со своего хоста**, без внешних. Бинарник самодостаточен,
|
||||
внешних ресурсов времени выполнения нет.
|
||||
Фреймворк, форма сборки, способ хранения состояния на клиенте, правила
|
||||
разбиения на компоненты, обращение к API и показ ошибок. Всё это — предмет
|
||||
`spa-framework-choice`; писать их наперёд, не зная фреймворка, значит написать
|
||||
правила, которые придётся выбросить второй раз.
|
||||
|
||||
+6
-2
@@ -14,7 +14,7 @@
|
||||
| Кто | Что ему нужно от нас |
|
||||
| --- | --- |
|
||||
| Владелец сервиса | Отправить голосовое сообщение из Telegram и получить текст ответом. Работает сегодня |
|
||||
| Приглашённый пользователь | Войти в веб через свою учётную запись, загрузить запись, забрать текст. Каждый видит только свои записи |
|
||||
| Приглашённый пользователь | Войти в приложение через свою учётную запись, загрузить запись, забрать текст. Приложение ставится на телефон; каждый видит только свои записи |
|
||||
| Внешняя программа | Отдать файл по HTTP и опросить готовность. Работает сегодня, без разграничения доступа |
|
||||
|
||||
Цель достигнута, когда:
|
||||
@@ -24,7 +24,8 @@
|
||||
- запись длиной в несколько часов доходит до текста, а не прерывается ошибкой при
|
||||
достижении предела;
|
||||
- сервисом пользуются несколько человек, и записи одного не видны другому;
|
||||
- текст доступен там же, где загружали, — в вебе и в Telegram.
|
||||
- текст доступен там же, где загружали, — в приложении и в Telegram, и человек
|
||||
узнаёт о его готовности, не держа приложение открытым.
|
||||
|
||||
## Что целью не является
|
||||
|
||||
@@ -45,6 +46,9 @@
|
||||
провайдер, свою регистрацию и свои пароли не делаем.
|
||||
- **Живая расшифровка.** Работаем с готовой записью, поток в реальном времени не
|
||||
обрабатываем.
|
||||
- **Диктофон.** Запись звука делает телефон, а приложение принимает готовый
|
||||
файл. Своей записи и работы без сети не делаем — граница цели
|
||||
[web-access](../tasks/items/web-access.md).
|
||||
|
||||
## Типовые сценарии
|
||||
|
||||
|
||||
+6
-2
@@ -113,13 +113,17 @@
|
||||
- изменение, трогающее конвейер задач целиком: состояние, воркер, шаг сервиса и
|
||||
колонку разом;
|
||||
- замена хранилища или переход на PocketBase — любой её кусок;
|
||||
- введение веб-UI: транспорт, шаблоны и статика одновременно;
|
||||
- каркас приложения: сборка фронтенда, раздача статики и шаг гейта разом;
|
||||
- изменение, трогающее оба входа сразу — Telegram и HTTP.
|
||||
|
||||
**Незнакомое здесь** (поднимает до `large`, ось формы решения):
|
||||
|
||||
- вход через OIDC и разграничение доступа: как связаны пользователь Telegram и
|
||||
пользователь веба, до начала работы назвать нельзя;
|
||||
пользователь приложения, до начала работы назвать нельзя;
|
||||
- всё, что делается на выбранном фреймворке впервые: форма решения нащупывается
|
||||
по ходу, пока конвенция веб-UI пуста;
|
||||
- установка на телефон: service worker перехватывает запросы, и что он кэширует,
|
||||
до работы назвать нельзя;
|
||||
- работа с записями в несколько часов: потолки внешних сервисов не замерены,
|
||||
форма решения зависит от замера;
|
||||
- приём дорожки из видео и форматов, которых `ffmpeg` не берёт текущей командой;
|
||||
|
||||
+11
-1
@@ -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
@@ -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) — Расшифровка занимает минуты, и всё это время человек либо смотрит на экран с опросом статуса, либо забывает вернуться.
|
||||
|
||||
## Направления
|
||||
|
||||
|
||||
@@ -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`.
|
||||
@@ -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`;
|
||||
задача про форму контракта. Публичный контракт после мерджа обратной правкой не
|
||||
откатывается.
|
||||
@@ -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` обязательна.
|
||||
@@ -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` открытыми? Прокси и система сбора
|
||||
метрик сессии не имеют, но и наружу их отдавать незачем.
|
||||
@@ -0,0 +1,25 @@
|
||||
# 🎯 Пользователь узнаёт о готовности текста, не держа приложение открытым
|
||||
|
||||
- **Тип:** goal
|
||||
- **Секция:** Запланировано
|
||||
- **Зачем:** Расшифровка занимает минуты, и всё это время человек либо смотрит на экран с опросом статуса, либо забывает вернуться.
|
||||
|
||||
Задача уходит в работу — человек закрывает приложение и получает сообщение,
|
||||
когда текст готов. Пользователь Telegram это уже имеет: бот отвечает сам.
|
||||
Пользователь веба — нет.
|
||||
|
||||
Доставку берём внешнюю: apprise как отправитель, ntfy как канал. Web Push с
|
||||
VAPID и собственным хранением подписок за целью **не стоит** — это отдельная
|
||||
инфраструктура ради того же результата.
|
||||
|
||||
## Завершение
|
||||
|
||||
1. Готовый текст доходит до пользователя веба сообщением, без открытого
|
||||
приложения.
|
||||
2. Отказ задачи доходит тем же путём и тем же человекочитаемым текстом, что
|
||||
видит пользователь Telegram.
|
||||
3. Адрес канала уведомлений задаёт пользователь, а не общий конфиг: у каждого
|
||||
свой.
|
||||
4. Недоступность канала уведомлений не роняет задачу и не мешает ей завершиться:
|
||||
текст остаётся в приложении.
|
||||
5. Пользователь Telegram получает ответ по-прежнему ботом, а не вторым каналом.
|
||||
@@ -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 и пустое тело.
|
||||
- Запись, заведённая из веба, принадлежит вошедшему; заведённая ботом —
|
||||
пользователю, за которым закреплён чат. Оракул — тест на оба входа со сверкой
|
||||
колонки владельца.
|
||||
- Выборка воркера владельцем **не** сужается: конвейер обрабатывает записи всех.
|
||||
Оракул — тест: задачи двух владельцев проходят конвейер одним воркером.
|
||||
- База заводится с чистого листа, колонка владельца обязательна и без умолчания.
|
||||
Оракул — прогон миграций на пустой базе и попытка вставки без владельца.
|
||||
|
||||
## Рамки
|
||||
|
||||
Совместного доступа, ролей и передачи записи другому не делаем: владелец один и
|
||||
неизменяем. Данные не переносим — чистый лист.
|
||||
@@ -0,0 +1,37 @@
|
||||
# ✨ Сделать экран списка своих записей и чтения текста
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Ядро
|
||||
- **Зачем:** Расшифровка сегодня доходит одним сообщением и теряется в переписке; вернуться к ней через неделю нечем.
|
||||
- **Теги:** goal:web-access
|
||||
|
||||
Двигает пункты 3 и 4 «Завершения» цели: список своих записей открывается и
|
||||
листается, а готовый текст читается и копируется целиком.
|
||||
|
||||
Берётся после `upload-and-status-screen`.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- новый экран списка и экран одной записи;
|
||||
- постраничное чтение из контракта API;
|
||||
- копирование текста в буфер обмена;
|
||||
- показ записи, которая ещё в работе, — тем же состоянием, что на экране
|
||||
загрузки.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Список показывает записи вошедшего, новые сверху, и листается дальше первой
|
||||
страницы. Оракул — тест экрана на подставном API с двумя страницами.
|
||||
- Текст готовой записи открывается целиком, без деления на части. Оракул — тест
|
||||
на записи с текстом длиннее предела сообщения Telegram: на экране весь текст.
|
||||
- Текст копируется одним действием. Оракул — тест: после действия в буфере обмена
|
||||
тот же текст.
|
||||
- Запись в работе показывается состоянием и доходит до текста без перезагрузки
|
||||
экрана. Оракул — тест: подставной API отвечает `transcribe`, затем `done`;
|
||||
экран переходит к тексту сам.
|
||||
|
||||
## Рамки
|
||||
|
||||
Поиска по тексту, переименования и удаления записей не делаем. Правки текста не
|
||||
делаем — это граница из [паспорта](../../docs/passport.md): расшифровку отдаём как
|
||||
есть.
|
||||
@@ -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` и
|
||||
цена шага сборки в гейте, а не популярность.
|
||||
@@ -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, и оно записывается.
|
||||
@@ -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` переписывается этой же задачей, а не остаётся врать.
|
||||
@@ -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`.
|
||||
@@ -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. Открытое без сети, приложение показывает это состоянием, а не пустой
|
||||
страницей и не ошибкой браузера.
|
||||
|
||||
Reference in New Issue
Block a user