tasks: спроектирован контракт приложения и переставлена голова очереди
- json-api-for-spa: адреса приложения уехали в своё пространство /app/, приём стал POST /app/audiorecords, опрос /api/status/:id убран, заведены список, карточка, текст, /app/me и /app/config; имя файла отправителя легло своей колонкой рядом с заголовком - три действия над записью — правка заголовка, возврат в работу и журнал событий — собраны задачей audiorecord-actions - голова очереди: контракт, каркас, экран загрузки, список, действия - в прежних задачах поправлены адреса, рубежи конвейера и остатки Telegram
This commit is contained in:
@@ -64,8 +64,13 @@
|
||||
экранов и какие — не здесь: состав нормирует спека приложения, а до неё его
|
||||
держит [spa-skeleton](../../tasks/items/spa-skeleton.md).
|
||||
- **Адреса обычные, а не после решётки** (`createWebHistory`). Отсюда требование
|
||||
к серверу: неизвестный путь **вне** `/api/` отдаёт `index.html`, а не `404`;
|
||||
пути внутри `/api/` в приложение не проваливаются никогда.
|
||||
к серверу: неизвестный путь **вне корней сервиса** отдаёт `index.html`, а не
|
||||
`404`; путь внутри корня в приложение не проваливается никогда. Корней
|
||||
сегодня четыре — `/api/` у хранилища, `/app/` у приложения, `/auth/` у входа,
|
||||
`/_/` у панели, — плюс `/health` и `/metrics` отдельными адресами. Приложение
|
||||
уехало из общего `/api/` решением владельца 2026-08-15: пространство
|
||||
принадлежит хранилищу, и обновление библиотеки вправе занять там имя рядом с
|
||||
нашим.
|
||||
- **Экран не знает, как он открыт.** Данные экран берёт по своему адресу, а не
|
||||
получает от предыдущего: приложение открывают по ссылке и обновляют страницу
|
||||
посередине.
|
||||
|
||||
+5
-4
@@ -43,6 +43,11 @@
|
||||
|
||||
## Очередь
|
||||
|
||||
- [✨ Свести приём и чтение записей к одному контракту для приложения](items/json-api-for-spa.md) — Сегодняшний API отвечает 404 на любую ошибку чтения и 500 на любую ошибку приёма: строить на нём экраны нечем.
|
||||
- [✨ Собрать каркас приложения и раздать его из бинарника](items/spa-skeleton.md) — Экранов нет и собирать их нечем: ни сборки фронтенда, ни раздачи статики в проекте не существует.
|
||||
- [✨ Сделать экран загрузки записи и её состояния](items/upload-and-status-screen.md) — Первое, ради чего приложение открывают: отдать файл и увидеть, что с ним происходит.
|
||||
- [✨ Сделать экран списка своих записей и чтения текста](items/records-list-screen.md) — Расшифровка сегодня доходит одним сообщением и теряется в переписке; вернуться к ней через неделю нечем.
|
||||
- [✨ Дать владельцу править запись, возвращать её в работу и видеть её путь](items/audiorecord-actions.md) — С записью нельзя сделать ничего: заголовок ставит одна языковая модель, остановленную возвращает в работу только владелец сервиса в панели, а журнал событий пишется и не читается никем, кроме него же
|
||||
- [🐞 Убрать код провайдера из журнала запросов хранилища](items/provider-code-out-of-storage-log.md) — Строка запроса с кодом входа целиком уезжает в таблицу _logs и лежит там пять суток, хотя спека access требует, чтобы код в журнал не попадал.
|
||||
- [🐞 Вести учёт употреблённых состояний входа на сервере](items/server-side-login-state.md) — Одноразовость возврата держится на уборке куки, то есть на браузере: сервер не помнит, какие состояния уже потрачены.
|
||||
- [✨ Строить адрес входа из настроек коллекции, а не из конфига](items/login-url-from-collection-settings.md) — Первая половина входа собрана руками из конфига и на настройки провайдера не смотрит, вторая берётся из коллекции: обновление библиотеки изменит только вторую половину.
|
||||
@@ -52,7 +57,6 @@
|
||||
- [🧹 Разобрать мелочи http-транспорта](items/http-transport-nits.md) — Маршруты зарегистрированы дважды, и переименование пути в main.go проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом.
|
||||
- [🧹 Запретить обращаться к Bot API мимо клиента бота](items/bot-api-only-through-bot-client.md) — Чистка отказа от адреса с токеном живёт в клиенте; свой http.Client в транспорте вернёт утечку молча — правило noctx такую подмену не ловит, а класс уже стоил одного дефекта.
|
||||
- [🧹 Свести пять расхождений между документами канона](items/docs-consistency-2026-08-13.md) — Сверка 2026-08-13 нашла шесть мест, где два документа отвечают на один вопрос по-разному; одно сведено при повышении раскладки, а три из пяти оставшихся стоят в architecture.md, и по ним читатель строит решения о выкладке и о периметре.
|
||||
- [✨ Свести приём и чтение записей к одному контракту для приложения](items/json-api-for-spa.md) — Сегодняшний API отвечает 404 на любую ошибку чтения и 500 на любую ошибку приёма: строить на нём экраны нечем.
|
||||
- [✨ Сопоставить пользователя Telegram с учётной записью](items/telegram-account-link.md) — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны.
|
||||
- [✨ Пускать скрипты в API по личным токенам](items/api-tokens.md) — Вход через OIDC закрывает API целиком, а скрипту браузерная сессия недоступна: автоматизировать загрузку станет нечем.
|
||||
- [🧹 Покрыть тестами шаги конвейера и захват задачи](items/pipeline-step-tests.md) — Тестовых файлов в проекте два, и оба мимо конвейера: потеря ссылки на файл, двойной ответ пользователю и гонка при захвате не поймаются ничем.
|
||||
@@ -63,9 +67,6 @@
|
||||
- [🐞 Убирать записанный файл, когда приём отказал на середине](items/orphan-file-on-failed-intake.md) — Отказ чтения метаданных и отказ записи на диск оставляют файл в каталоге хранения без задачи и без учёта: сопоставить его не с чем, удалять приходится руками.
|
||||
- [🧹 Разобрать мелочи слоя хранилища](items/storage-layer-nits.md) — Три мелочи ниже потолка триажа: цикл воркера пишет потерю захвата уровнем ERROR и считает её отказом, тип ошибки заведён там, где конвенция просит sentinel, а FileName несёт два разных смысла.
|
||||
- [🧹 Закрепить версию рантайм-базы образа](items/pin-runtime-image-base.md) — Финальный слой Dockerfile собирается на alpine:latest, а task image идёт с --pull, поэтому два образа из одного коммита с разницей в неделю несут разный ffmpeg — регрессия конвертации после такой пересборки выглядит как задачи в failed при пустом диффе репозитория, и откат на прежний коммит её не чинит.
|
||||
- [✨ Собрать каркас приложения и раздать его из бинарника](items/spa-skeleton.md) — Экранов нет и собирать их нечем: ни сборки фронтенда, ни раздачи статики в проекте не существует.
|
||||
- [✨ Сделать экран загрузки записи и её состояния](items/upload-and-status-screen.md) — Первое, ради чего приложение открывают: отдать файл и увидеть, что с ним происходит.
|
||||
- [✨ Сделать экран списка своих записей и чтения текста](items/records-list-screen.md) — Расшифровка сегодня доходит одним сообщением и теряется в переписке; вернуться к ней через неделю нечем.
|
||||
- [✨ Проигрывать загруженную запись на экране записи](items/play-recording-in-app.md) — Послушать загруженное приложение не даёт, а самой копии для этого у задачи нет: указатель на файл перезаписывается на каждом шаге конвейера и у готовой задачи ведёт на объект в Object Storage.
|
||||
- [✨ Сделать приложение устанавливаемым на телефон](items/installable-pwa.md) — Приложение, живущее вкладкой браузера, теряется среди прочих: ярлыка на экране у него нет.
|
||||
- [✨ Узнавать уже загруженный файл по хеш-сумме](items/dedup-by-content-hash.md) — Один и тот же файл, отправленный дважды, распознаётся дважды и оплачивается дважды: приём не смотрит на содержимое вовсе.
|
||||
|
||||
@@ -22,3 +22,6 @@
|
||||
- 2026-08-13 `gate-steps-subject-guard` — 🔬 Шаги гейта, у которых правило может потерять предмет. Причина: Владелец отменил 2026-08-13: разведка того же класса — проверка над проверками. Ведут ли себя шаги docs, tasks и openspec зелёными без предмета, остаётся неизвестным. Была секция: Очередь.
|
||||
- 2026-08-13 `review-config-from-go-upgrade` — 🧹 Настроить конвейер ревью по итогам прогона go-1-26-upgrade. Причина: Владелец отменил 2026-08-13: настройка конвейера ревью даёт много механики и мало пользы. Разделы «Типовые узлы» и «Триггеры метки» в docs/review.md остаются как есть. Была секция: Очередь.
|
||||
- 2026-08-13 `rollback-does-not-undo-schema-step` — 🧹 Назвать в необратимом, что откат кода не откатывает шаг схемы. Причина: Владелец отменил 2026-08-13: задача целиком документационная — одна строка в «Необратимое» о том, что откат бинаря не откатывает шаг схемы. Факт остаётся неназванным нигде. Была секция: Очередь.
|
||||
- 2026-08-15 `rename-audiorecord` — ✨ Дать владельцу записи переименовать её. Причина: Слилась в audiorecord-actions 2026-08-15: правка заголовка живёт на экране одной записи вместе с возвратом в работу и журналом событий, и порознь каждое действие несло бы свою правку того же экрана. Была секция: Очередь.
|
||||
- 2026-08-15 `resume-halted-record` — ✨ Вернуть остановленную запись в работу из приложения. Причина: Слилась в audiorecord-actions 2026-08-15: возврат в работу живёт на экране одной записи вместе с правкой заголовка и журналом событий. Была секция: Очередь.
|
||||
- 2026-08-15 `record-events-screen` — ✨ Показывать журнал событий записи её владельцу. Причина: Слилась в audiorecord-actions 2026-08-15: журнал событий читается с экрана одной записи вместе с правкой заголовка и возвратом в работу. Была секция: Очередь.
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- заголовок авторизации у всех эндпоинтов `/api/`;
|
||||
- заголовок авторизации у всех адресов приложения `/app/`;
|
||||
- таблица токенов: владелец, имя, отпечаток, время выпуска и последнего
|
||||
обращения, и её миграция;
|
||||
- эндпоинты выпуска, перечня и отзыва токена;
|
||||
- адреса выпуска, перечня и отзыва токена — `/app/me/tokens`;
|
||||
- `docs/security.md` — второй способ представиться и хранение отпечатка;
|
||||
- `README.md` — пример вызова API скриптом.
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# ✨ Дать владельцу править запись, возвращать её в работу и видеть её путь
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь — Три действия живут на экране одной записи, а заводит его список
|
||||
- **Зачем:** С записью нельзя сделать ничего: заголовок ставит одна языковая модель, остановленную возвращает в работу только владелец сервиса в панели, а журнал событий пишется и не читается никем, кроме него же
|
||||
|
||||
Экран одной записи перестаёт быть окном просмотра: с него запись переименовывают,
|
||||
возвращают в работу после отказа и смотрят её путь по рубежам.
|
||||
|
||||
Три действия собраны одной задачей, потому что живут на одном экране и стоят на
|
||||
одном контракте: порознь каждое несло бы свой адрес и свою правку того же экрана.
|
||||
Собраны они после решения владельца 2026-08-15, которым разведены колонки
|
||||
заголовка и имени файла и назначено пространство адресов `/app/`.
|
||||
|
||||
**Переименование.** Колонка заголовка объявлена местом для «пользовательского или
|
||||
сгенерированного» названия, а поставить его человеку нечем: заполняет её один
|
||||
`llm-insights-adapter`. Отсюда же второй вопрос, который решается здесь: что
|
||||
происходит с заголовком, поставленным человеком, когда модель посчитает свой.
|
||||
|
||||
**Возврат в работу.** Паспорт описывает сценарий «отказ на середине» и кончает
|
||||
его тем, что владелец записи узнаёт о неудаче. Дальше пусто: разовый сбой
|
||||
`ffmpeg` оставляет запись остановленной, пока владелец сервиса не откроет панель.
|
||||
Само снятие остановки написано — `entity.Resume` чистит все три сторожа и ставит
|
||||
время входа в рубеж заново. Цена названа прямо: повтор запускает платное
|
||||
распознавание, и запускает его теперь пользователь, а не владелец сервиса —
|
||||
значит у возвратов нужен предел, и его величину выбирает эта задача.
|
||||
|
||||
**Путь записи.** Журнал событий заполняется на смену рубежа, остановку и снятие
|
||||
остановки, а читается только в панели: `architecture.md` говорит про него прямо,
|
||||
что «экрана у него пока нет». Владельцу записи он нужен там, где карточка
|
||||
отвечает «остановлена»: без пути «встала сразу» неотличимо от «висела два часа».
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- `PATCH /app/audiorecords/{id}` — правка заголовка, и только его;
|
||||
- `POST /app/audiorecords/{id}/resume` — возврат остановленной записи в работу;
|
||||
- `GET /app/audiorecords/{id}/events` — события своей записи по времени;
|
||||
- предел числа возвратов и место, где он считается: колонка записи либо журнал
|
||||
событий;
|
||||
- ответ карточки записи — признак того, можно ли вернуть запись в работу сейчас;
|
||||
- шаг конвейера, кладущий посчитанный заголовок: признак того, что заголовок
|
||||
поставлен человеком;
|
||||
- экран одной записи из `records-list-screen`: три действия и показ пути;
|
||||
- перевод рубежей и причин остановки в текст для человека — тот же словарь, что
|
||||
на карточке;
|
||||
- спека `pipeline` — возврат в работу нормирован там со стороны панели;
|
||||
спека `storage` — журнал событий описан там как канал владельца сервиса;
|
||||
правка записи её владельцем не нормирована нигде.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Заголовок правится владельцем и переживает перезагрузку экрана, посчитанный
|
||||
его не затирает, а пустой возвращает показ по имени файла отправителя. Оракул
|
||||
— три теста: правка и чтение карточки; шаг расчёта заголовка по записи с
|
||||
правленым заголовком; правка пустым значением и чтение списка.
|
||||
- Заголовок ограничен длиной и очищен от управляющих знаков — теми же правилами,
|
||||
какими приём чистит имя файла. Оракул — тест на строке сверх предела и со
|
||||
знаками управления.
|
||||
- Остановленная запись возвращается в работу с сохранённого рубежа с очищенными
|
||||
сторожами, неостановленная отвечает отказом, а возвраты сверх предела — тоже,
|
||||
и запись остаётся остановленной. Оракул — три теста: возврат записи с
|
||||
накопленными отказами и признаком прежнего захвата, затем захват с того же
|
||||
рубежа; возврат записи в работе; возвраты сверх предела.
|
||||
- Владелец видит события своей записи по времени, и машинного текста отказа в
|
||||
них нет: событие несёт причину остановки, а не сообщение зависимости. Оракул —
|
||||
два теста: запись, прошедшая два рубежа и остановку; остановленная запись с
|
||||
текстом ошибки в колонке.
|
||||
- Чужая запись на всех трёх адресах отвечает тем же, чем несуществующая, — и
|
||||
кодом, и телом. Оракул — тест на двух учётных записях, по разу на каждый
|
||||
адрес.
|
||||
|
||||
## Рамки
|
||||
|
||||
Правку прочих полей записи не заводим: меняется один заголовок. Краткое описание
|
||||
и темы остаются за языковой моделью — их правка руками это отдельное решение,
|
||||
которого никто не принимал. Панель владельца сервиса остаётся как есть: она
|
||||
возвращает записи без предела, потому что деньгами распоряжается он. Правил, по
|
||||
которым отказ отличается от приговора, не трогаем — это
|
||||
`failure-verdict-vs-retry`. Событий не добавляем и журнал не переписываем:
|
||||
показываем записанное.
|
||||
|
||||
Берётся после `records-list-screen` — экран одной записи заводит она.
|
||||
@@ -4,38 +4,39 @@
|
||||
- **Категория:** Очередь — Дедупликация ищет совпадение в пределах пользователя — то есть после владельца записи, и экономит деньги с первого дня приложения.
|
||||
- **Зачем:** Один и тот же файл, отправленный дважды, распознаётся дважды и оплачивается дважды: приём не смотрит на содержимое вовсе.
|
||||
|
||||
Повторная отправка того же файла возвращает прежнюю запись вместо второй задачи.
|
||||
Повторная отправка того же файла возвращает прежнюю запись вместо второй.
|
||||
|
||||
Совпадение ищется **в пределах одного пользователя**: чужая расшифровка по
|
||||
совпадению хеш-суммы не отдаётся и о её существовании отправитель не узнаёт.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- `TranscribeService.createTranscribeJob` — единая точка приёма, через неё идут
|
||||
оба входа;
|
||||
- `TranscribeService.createRecord` — единственный путь, которым запись
|
||||
появляется в хранилище;
|
||||
- таблица `files`: колонка хеш-суммы, её миграция и индекс по паре «владелец,
|
||||
хеш-сумма»;
|
||||
- контракт `POST /api/audio`: ответ на повторный файл;
|
||||
- ответ бота на повторно присланное голосовое;
|
||||
- контракт `POST /app/audiorecords`: признак повторного файла в ответе — место
|
||||
под него согласовано задачей `json-api-for-spa`;
|
||||
- `docs/database.md` — представление данных.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Повторная отправка того же файла тем же пользователем возвращает
|
||||
идентификатор прежней задачи, второй задачи в базе не появляется. Оракул —
|
||||
тест приёма: два вызова одним содержимым, в репозитории одна задача.
|
||||
- Тот же файл от другого пользователя заводит свою задачу и своё распознавание.
|
||||
Оракул — тест приёма с двумя владельцами: две задачи, тексты не разделяются.
|
||||
идентификатор прежней записи с признаком повтора, второй записи в базе не
|
||||
появляется. Оракул — тест приёма: два вызова одним содержимым, в репозитории
|
||||
одна запись, во втором ответе признак повтора.
|
||||
- Тот же файл от другого пользователя заводит свою запись и своё распознавание.
|
||||
Оракул — тест приёма с двумя владельцами: две записи, тексты не разделяются.
|
||||
- Повторный файл не остаётся вторым экземпляром в каталоге хранения. Оракул —
|
||||
тест: после второго вызова в каталоге один файл.
|
||||
- Незавершённая задача тоже узнаётся: повторная отправка отдаёт её состояние, а
|
||||
не заводит соседнюю. Оракул — тест на задаче в состоянии `created`.
|
||||
- Незавершённая запись тоже узнаётся: повторная отправка отдаёт её рубеж, а не
|
||||
заводит соседнюю. Оракул — тест на записи с рубежом `uploaded`.
|
||||
|
||||
## Рамки
|
||||
|
||||
Владелец записи приходит из `record-ownership` — до неё дедупликация опирается
|
||||
на того владельца, который уже есть. Хеш-сумма считается на сервере: подсчёт на
|
||||
Владелец записи обязателен с 2026-08-14 — его держит схема хранилища, — и
|
||||
дедупликация опирается на него. Хеш-сумма считается на сервере: подсчёт на
|
||||
стороне приложения относится к `chunked-upload-choice`. Колонка добавляется во
|
||||
всех четырёх местах пакета хранилища `internal/adapter/repo/pocketbase` —
|
||||
`applyToRecord`, `recordToJob`, `acquireColumns`, `acquiredRow` с её `toJob`, —
|
||||
плюс шаг схемы (инвариант `CLAUDE.md`).
|
||||
всех местах пакета хранилища `internal/adapter/repo/pocketbase`, которые
|
||||
перечисляет инвариант `CLAUDE.md` «Колонки записи правятся в двух местах», плюс
|
||||
шаг схемы.
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
## Критерии приёмки
|
||||
|
||||
- Пути и методы объявлены в одном месте, и переименование пути роняет проверки.
|
||||
Оракул — мутация: заменить `/api/audio` на `/api/upload`, прогнать
|
||||
Оракул — мутация: заменить `/app/audiorecords` на `/app/uploads`, прогнать
|
||||
`go test ./internal/controller/http/` и увидеть красное.
|
||||
- Обработчик не пишет в журнал через стандартный `log`. Оракул —
|
||||
`grep -rn 'log\.' internal/controller/http/*.go` без импорта `log/slog` пуст.
|
||||
|
||||
@@ -60,7 +60,8 @@ file name too long"`, и поля `file_ext` в ней нет вовсе.
|
||||
|
||||
## Рамки
|
||||
|
||||
Приведение пути не должно делать маршрут неразличимым: по журналу отличают
|
||||
`/api/audio` от `/api/status/:id`. Найдено прогоном ревью задачи
|
||||
Приведение пути не должно делать маршрут неразличимым: по журналу отличают приём
|
||||
записи от чтения одной записи — `POST /app/audiorecords` от
|
||||
`GET /app/audiorecords/{id}`. Найдено прогоном ревью задачи
|
||||
`remove-telegram-intake` 2026-08-15, враждебный проход; дефект существовал до той
|
||||
правки, и владелец решил вынести его задачей.
|
||||
|
||||
+138
-18
@@ -1,7 +1,7 @@
|
||||
# ✨ Свести приём и чтение записей к одному контракту для приложения
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь — Единая точка трансляции доменной ошибки — база и для экранов, и для токенов; список своих записей заводится после владельца, а не до.
|
||||
- **Категория:** Очередь — Проект экранов и адресов согласован с владельцем 2026-08-15: на этом контракте стоят каркас приложения и все его экраны, а стоящие выше правки входа и хранилища его не подпирают
|
||||
- **Зачем:** Сегодняшний API отвечает 404 на любую ошибку чтения и 500 на любую ошибку приёма: строить на нём экраны нечем.
|
||||
|
||||
Экраны заводят задачу, видят её состояние и листают список — всё через один
|
||||
@@ -20,35 +20,144 @@
|
||||
предъявления отвергает такого вызывающего раньше. Он становится достижимым вместе
|
||||
с личными токенами и экранами приложения, то есть ровно здесь.
|
||||
|
||||
**Состав контракта владелец согласовал 2026-08-15**, разобрав его вместе с
|
||||
экранами приложения. Четыре развилки закрыты решением:
|
||||
|
||||
- **Опрос `GET /api/status/:id` убирается целиком**, а с ним и имена его полей:
|
||||
`job_id` зовётся `id`, текст переезжает на свой адрес. Стадия проекта —
|
||||
стройка, данных на сервере нет, а внешней программы на прежнем контракте не
|
||||
существует: своего токена у неё не было, и держать два адреса на один вопрос
|
||||
не за чем. Ломка объявляется в спеке `intake`.
|
||||
- **Запись подписывается именем файла, данным отправителем.** Имя ложится
|
||||
**своей колонкой** — `original_filename`, — а колонка заголовка остаётся под
|
||||
название, которое дал человек либо посчитала языковая модель. Одной колонкой
|
||||
на оба смысла посчитанный заголовок затирал бы то, по чему человек узнаёт свою
|
||||
запись, и вернуть затёртое было бы неоткуда. Ответ несёт оба поля, а экран
|
||||
показывает заголовок и подставляет имя файла, пока заголовка нет. Норма
|
||||
`intake` сужается: имя не доходит до **имени файла** в хранилище и до журнала,
|
||||
а в самой записи его видит один владелец. Отсюда обязанность приёма: длину
|
||||
ограничить, управляющие знаки убрать.
|
||||
- **Аудио отдаётся своим адресом**, а не ссылкой хранилища с токеном файла:
|
||||
норма `storage` запрещает ссылке выходить в ответ отправителю.
|
||||
- **Текст едет отдельным адресом**, а не вместе с карточкой: шестичасовая
|
||||
расшифровка иначе задерживает показ шапки записи на мобильной сети.
|
||||
|
||||
Адреса приложения после этого такие:
|
||||
|
||||
| Метод и путь | Что отдаёт |
|
||||
| --- | --- |
|
||||
| `GET /app/me` | кто вошёл |
|
||||
| `GET /app/config` | чем сервер ограничивает: потолок размера записи, частота опроса, перечень известных расширений, потолок тем |
|
||||
| `POST /app/audiorecords` | список заведённых записей |
|
||||
| `GET /app/audiorecords` | страницу своих записей, новые сверху |
|
||||
| `GET /app/audiorecords/{id}` | карточку записи без текста |
|
||||
| `GET /app/audiorecords/{id}/text` | текст названного вида — сплошной либо репликами со временем |
|
||||
|
||||
`GET /app/config` заводится здесь же, а не задачей экрана: пределы объявляет
|
||||
сервер, и экран, знающий их своей константой, расходится с ним молча — до
|
||||
первого отказа на гигабайтном файле, который человек уже успел отправить.
|
||||
|
||||
Приём стоит тем же адресом, что и список, и отличается только методом: он
|
||||
**заводит аудиозапись**, а не кладёт файл. Прежнее имя `audio` называло
|
||||
содержимое запроса, и по нему приём читался как отдельная от записи вещь —
|
||||
хотя запись он и создаёт.
|
||||
|
||||
Элемент списка несёт идентификатор, заголовок, имя файла отправителя, краткое
|
||||
описание, темы, рубеж, признак остановки и её причину, длительность, размер и
|
||||
время заведения.
|
||||
Длительность с размером лежат сегодня строкой файла, а список по норме `storage`
|
||||
читается без содержимого — значит оба переезжают колонками записи и заполняются
|
||||
приёмом, который читает их у `ffprobe` и так.
|
||||
|
||||
Адреса записи названы `audiorecords` — по центральной сущности сервиса, а не
|
||||
общим словом «записи»: последнее в хранилище значит строку любой коллекции.
|
||||
Коллекция при этом зовётся `audio_records`, и расхождение намеренное: в схеме
|
||||
имена в snake_case, у соседей по адресу — слитно.
|
||||
|
||||
**Приложение целиком уезжает из `/api/` в своё пространство `/app/`** — решение
|
||||
владельца 2026-08-15. Пространство `/api/` принадлежит хранилищу: оно вешает
|
||||
туда двенадцать наборов своих адресов (`settings`, `collections`, `files`,
|
||||
`logs`, `realtime`, `batch`, `backups`, `crons`, `health`, `sql`,
|
||||
`oauth2-redirect` и собственные адреса входа коллекций), и поменять этот префикс
|
||||
нельзя — он литерал в `apis/base.go`, а не настройка. Свободных имён сегодня
|
||||
хватает, но соседство остаётся: обновление библиотеки вправе занять новое имя
|
||||
рядом с нашим, и разойдутся они молча. Прецедент в проекте уже принят — адреса
|
||||
входа вынесены на `/auth/*` этим же доводом (спека `access`).
|
||||
|
||||
Цена переезда названа: правило неизвестного пути перечисляет теперь четыре корня
|
||||
— `/api/`, `/app/`, `/auth/` и `/_/` — вместо одного, а ограничитель частоты
|
||||
хранилища, настроенный на `/api/`, наших адресов больше не покрывает, и своё
|
||||
правило под `/app/` заводится здесь же.
|
||||
|
||||
Настройки пользователя лягут на `/app/me/settings`; собственный `/api/settings`
|
||||
принадлежит хранилищу и остаётся ему.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- `POST /api/audio` и `GET /api/status/:id` — коды ответа и форма ошибки;
|
||||
- новый эндпоинт списка своих записей с постраничным чтением;
|
||||
- `POST /app/audiorecords` — переезд приёма из `/api/audio`, коды ответа, форма
|
||||
ошибки и форма успешного ответа;
|
||||
- `GET /api/status/:id` — убирается вместе со своими именами полей;
|
||||
- `GET /app/me` — кто вошёл;
|
||||
- `GET /app/config` — пределы и свойства сервера для экранов;
|
||||
- `GET /app/audiorecords` — список своих записей с постраничным чтением и отбором по
|
||||
тому, в работе запись или нет;
|
||||
- `GET /app/audiorecords/{id}` и `GET /app/audiorecords/{id}/text` — карточка и текст
|
||||
названного вида, сплошной либо репликами со временем;
|
||||
- слой предъявления сессии и своё правило ограничителя частоты — оба переезжают
|
||||
с адресов на новый корень;
|
||||
- колонки `original_filename`, длительности и размера у аудиозаписи, и шаг схемы
|
||||
под них;
|
||||
- `internal/entity` и три места правки колонок в `internal/adapter/repo/pocketbase`
|
||||
— компилятор их расхождения не видит (инвариант «Колонки записи правятся в
|
||||
двух местах» в [CLAUDE.md](../../CLAUDE.md));
|
||||
- приём: имя файла отправителя в свою колонку, с ограничением длины и уборкой
|
||||
управляющих знаков;
|
||||
- единая точка отображения доменной ошибки в код ответа — её сегодня нет
|
||||
([conventions/errors.md](../../docs/conventions/errors.md));
|
||||
- `internal/contract` — доменные ошибки под отображение;
|
||||
- `internal/contract` — доменные ошибки под отображение, отбор списка по
|
||||
владельцу;
|
||||
- `internal/controller/http` целиком;
|
||||
- `docs/architecture.md`, раздел «Единые точки проекта».
|
||||
- спека `intake` — новый адрес приёма, убранный опрос готовности и суженная
|
||||
норма про имя отправителя;
|
||||
- спека `access` — область слоя предъявления сессии названа там адресами
|
||||
приложения;
|
||||
- `docs/conventions/web-ui.md` — правило неизвестного пути перечисляет корни
|
||||
сервиса, а не один `/api/`;
|
||||
- `docs/architecture.md`, раздел «Единые точки проекта»;
|
||||
- `docs/database.md` — новые колонки записи.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Код ответа отвечает причине отказа, а не месту, где он случился: сбой базы при
|
||||
чтении даёт `500`, а не `404`, негодный файл — `400` с человекочитаемым
|
||||
текстом, а не `500`, а отказ приёма по пустому владельцу — `403`, как и отказ
|
||||
слоя предъявления. Оракул — три теста: репозиторий, возвращающий ошибку
|
||||
драйвера; файл, который отвергает разбор метаданных; вызов приёма без учётной
|
||||
записи.
|
||||
- Тело ошибки одной формы на всех эндпоинтах, не содержит сырого `err.Error()` и
|
||||
опечатки `transcibe`. Оракул — тест на четырёх ветвях отказа (форма совпадает,
|
||||
текста внутренней ошибки в теле нет) плюс пустой `grep -rn 'transcibe'
|
||||
internal/`.
|
||||
- Список записей отдаётся страницами и упорядочен по времени создания. Оракул —
|
||||
тест на выборке больше страницы.
|
||||
- Отображение ошибки живёт в одной функции, и она названа в
|
||||
`docs/architecture.md`. Оракул — `task gate`, шаг `docs.py check`.
|
||||
слоя предъявления. Тело ошибки при этом одной формы на всех адресах, без
|
||||
сырого `err.Error()` и без опечатки `transcibe`, а отображение живёт в одной
|
||||
функции, названной в `docs/architecture.md`. Оракул — три теста на код
|
||||
(репозиторий с ошибкой драйвера; файл, который отвергает разбор метаданных;
|
||||
вызов приёма без учётной записи), тест формы на четырёх ветвях отказа, пустой
|
||||
`grep -rn 'transcibe' internal/` и `task gate`, шаг `docs.py check`.
|
||||
- Список отдаётся страницами, новые сверху, и несёт заголовок, имя файла
|
||||
отправителя, длительность, размер, рубеж с признаком остановки и темы, не
|
||||
читая при этом ни расшифровки, ни структуры реплик. Оракул — два теста:
|
||||
выборка больше страницы; запись с расшифровкой, у которой чтение списка не
|
||||
трогает строку текста.
|
||||
- Ответ приёма отдаёт список заведённых записей и место под признак повторного
|
||||
файла, даже когда файл в запросе один. Оракул — тест приёма: тело ответа —
|
||||
список из одного элемента, у элемента есть поле признака повтора.
|
||||
файла, даже когда файл в запросе один, а имя файла отправителя лежит своей
|
||||
колонкой — обрезанное по пределу и без управляющих знаков — при пустом
|
||||
заголовке. Оракул — четыре теста приёма: тело ответа — список из одного
|
||||
элемента с полем признака повтора; имя `разговор.mp3` доходит до
|
||||
`original_filename`; колонка заголовка у принятой записи пуста; имя длиннее
|
||||
предела и с управляющими знаками доходит обрезанным и очищенным.
|
||||
- `GET /app/config` отдаёт потолок размера тем же числом, каким сервер отвергает
|
||||
запись сверх него, а не своей копией. Оракул — тест: значение в ответе равно
|
||||
`entity.MaxRecordSize`, и подмена константы меняет ответ.
|
||||
- Карточка записи и её текст читаются порознь, а прежний опрос готовности
|
||||
отвечает `404`: карточка несёт признак наличия текста, а текст отдаётся
|
||||
названным видом — сплошным либо репликами со временем. Оракул — четыре теста:
|
||||
карточка без поля текста; текст вида `transcript` сплошным; он же репликами со
|
||||
временем; прежние адреса `GET /api/status/{id}` и `POST /api/audio` на
|
||||
заведённой записи отвечают `404`.
|
||||
|
||||
## Рамки
|
||||
|
||||
@@ -62,3 +171,14 @@
|
||||
Сюда же приехала опечатка `transcibe` в тексте ошибки приёма (нашёл проход `review-specs` на ревью change
|
||||
`2026-08-11-fix-http-handler-tests`): в одиночку текст ошибки менять нельзя,
|
||||
а здесь контракт переписывается целиком.
|
||||
|
||||
Экранов не делаем ни одного — их берут `spa-skeleton` и задачи экранов. Три
|
||||
адреса контракт называет, но не реализует, потому что у каждого свой хозяин:
|
||||
поток аудио `GET /app/audiorecords/{id}/audio` — `play-recording-in-app`, чтение и
|
||||
запись настроек по адресу `/app/me/settings` — `settings-screen`, признак
|
||||
повторного файла — `dedup-by-content-hash`. Заголовок и темы, которые считает
|
||||
языковая модель, тоже не здесь: место под них в ответе есть, заполняет его
|
||||
`llm-insights-adapter`. Имени файла отправителя он при этом не трогает — колонки
|
||||
разные, и вопроса «что затрёт что» у них не возникает. Правки заголовка руками в
|
||||
приложении здесь тоже нет: колонка под неё заведена, а экран и адрес принесёт
|
||||
задача, которой это понадобится.
|
||||
|
||||
@@ -8,30 +8,33 @@
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- контракт `POST /api/audio`: несколько файлов в одной форме и ответ списком;
|
||||
- `TranscribeService.createTranscribeJob` — заведение нескольких задач одним
|
||||
- контракт `POST /app/audiorecords`: несколько файлов в одной форме и ответ
|
||||
списком;
|
||||
- `TranscribeService.createRecord` — заведение нескольких записей одним
|
||||
запросом;
|
||||
- экран загрузки: выбор нескольких файлов и показ их состояний;
|
||||
- экран загрузки: выбор нескольких файлов и показ их рубежей;
|
||||
- предел числа файлов и предел размера запроса на стороне сервера;
|
||||
- `GET /app/config` — предел числа файлов, чтобы экран знал его до отправки;
|
||||
- `docs/database.md` — предел числа файлов числом.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Десять файлов одной формой заводят десять задач, и ответ отдаёт
|
||||
идентификатор каждой. Оракул — тест приёма: в ответе десять записей, в
|
||||
репозитории десять задач.
|
||||
- Десять файлов одной формой заводят десять записей, и ответ отдаёт
|
||||
идентификатор каждой. Оракул — тест приёма: в ответе десять элементов, в
|
||||
репозитории десять записей.
|
||||
- Негодный файл в пачке отклоняется поимённо, а годные соседи заводятся. Оракул
|
||||
— тест на пачке из годного и негодного: одна задача заведена, второй элемент
|
||||
— тест на пачке из годного и негодного: одна запись заведена, второй элемент
|
||||
ответа несёт причину отказа.
|
||||
- Одиннадцатый файл отклоняется до чтения содержимого, с названным пределом.
|
||||
Оракул — тест на пачке из одиннадцати: задач не заведено, в теле ответа
|
||||
Оракул — тест на пачке из одиннадцати: записей не заведено, в теле ответа
|
||||
предел числом.
|
||||
- Прежний вызов с одним файлом работает по-старому. Оракул — существующие тесты
|
||||
приёма по HTTP.
|
||||
|
||||
## Рамки
|
||||
|
||||
Публичный контракт `POST /api/audio` меняется, а это необратимо — форма ответа
|
||||
согласуется с человеком. Загрузка частями сюда не входит: это
|
||||
`chunked-upload-choice`. Приём из Telegram не трогается — бот присылает файлы по
|
||||
одному.
|
||||
Публичный контракт `POST /app/audiorecords` меняется, а это необратимо. Форма
|
||||
ответа при этом заново не согласуется: она сведена списком с местом под признак
|
||||
повтора задачей `json-api-for-spa` — решением владельца от 2026-08-12 — как раз
|
||||
для того, чтобы эта задача её не переписывала. Меняется число файлов в запросе,
|
||||
не форма ответа. Загрузка частями сюда не входит: это `chunked-upload-choice`.
|
||||
|
||||
@@ -18,12 +18,14 @@ ogg после конвертации и запись об объекте, —
|
||||
|
||||
- связь задачи с копией: сегодня `transcribe_jobs.file` — одно поле на одну
|
||||
копию, и шаг схемы придётся добавлять;
|
||||
- контракт HTTP API из `json-api-for-spa` — адрес проигрываемой копии в ответе
|
||||
о записи;
|
||||
- контракт HTTP API из `json-api-for-spa` — признак наличия аудио в ответе о
|
||||
записи;
|
||||
- экран одной записи из `records-list-screen`;
|
||||
- отдачу файла хранилищем: `/api/files/...` отвечает на `Range` сам, но
|
||||
`audio/ogg` не входит в перечень типов, которые хранилище отдаёт с
|
||||
`Content-Disposition: inline`, — ogg-копия уходит вложением;
|
||||
- `GET /app/audiorecords/{id}/audio` — свой адрес отдачи копии, с поддержкой
|
||||
`Range`: ссылка хранилища в ответ не выходит, это норма `storage`. Отдачу
|
||||
пишем сами, и оба свойства чужого адреса переходят к нам обязанностью — ответ
|
||||
на `Range` и тип содержимого, при котором копия проигрывается, а не
|
||||
скачивается вложением;
|
||||
- периметр из [ADR-2026-08-12](../../docs/adr/ADR-2026-08-12-file-link-open-but-not-logged.md):
|
||||
ссылка на файл открыта знанием записи, и экран делает её видимой странице;
|
||||
- `docs/architecture.md` — какая копия считается проигрываемой.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ✨ Сделать экран списка своих записей и чтения текста
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь — Список и чтение текста берутся после экрана загрузки — так записано в самой задаче.
|
||||
- **Категория:** Очередь — Список и экран одной записи берутся после экрана загрузки
|
||||
- **Зачем:** Расшифровка сегодня доходит одним сообщением и теряется в переписке; вернуться к ней через неделю нечем.
|
||||
|
||||
Список своих записей открывается и листается, а готовый текст читается и
|
||||
@@ -13,32 +13,40 @@
|
||||
|
||||
- новый экран списка и экран одной записи;
|
||||
- постраничное чтение из контракта API;
|
||||
- подпись записи в строке списка: заголовок, а пока его нет — имя файла
|
||||
отправителя;
|
||||
- чтение текста отдельным адресом и показ его репликами со временем;
|
||||
- копирование текста в буфер обмена;
|
||||
- показ записи, которая ещё в работе, — тем же состоянием, что на экране
|
||||
загрузки.
|
||||
- показ записи, которая ещё в работе, — тем же рубежом, что на экране загрузки.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Список показывает записи вошедшего, новые сверху, и листается дальше первой
|
||||
страницы. Оракул — тест экрана на подставном API с двумя страницами.
|
||||
- Строка списка подписана заголовком, а у записи без заголовка — именем файла
|
||||
отправителя. Оракул — тест на двух записях: с заголовком и без.
|
||||
- Текст готовой записи открывается целиком, без деления на части. Оракул — тест
|
||||
на записи с текстом длиннее предела сообщения Telegram: на экране весь текст.
|
||||
на записи с текстом в сотни килобайт: на экране весь текст.
|
||||
- Текст копируется одним действием. Оракул — тест: после действия в буфере обмена
|
||||
тот же текст.
|
||||
- Запись в работе показывается состоянием и доходит до текста без перезагрузки
|
||||
экрана. Оракул — тест: подставной API отвечает `transcribe`, затем `done`;
|
||||
экран переходит к тексту сам.
|
||||
- Запись в работе показывается рубежом и доходит до текста без перезагрузки
|
||||
экрана. Оракул — тест: подставной API отвечает рубежом `submitted`, затем
|
||||
`done`; экран переходит к тексту сам.
|
||||
|
||||
**Учесть от ревью 2026-08-12:** имя файла в хранилище несёт хвост имени, данного
|
||||
отправителем, — расширение берётся из него дословно, и запись
|
||||
`разговор.тайное-слово` ложится на диск именем с этим хвостом. Сегодня наружу
|
||||
оно не выходит: изъятие из инварианта приватности кончается журналом владельца.
|
||||
Экран списка это меняет — имя попадает в ссылку на скачивание и в заголовок
|
||||
ответа, то есть на страницу. Выхода за каталог хранения при этом нет, проверено
|
||||
пятнадцатью враждебными именами.
|
||||
`разговор.тайное-слово` ложится на диск именем с этим хвостом. Выхода за каталог
|
||||
хранения при этом нет, проверено пятнадцатью враждебными именами. Наружу это имя
|
||||
не выходит и теперь: аудио отдаётся своим адресом по идентификатору записи, а не
|
||||
ссылкой хранилища, и имени файла в ней нет. На страницу попадает другое — имя,
|
||||
данное отправителем, своей колонкой записи, и попадает намеренно: по нему человек
|
||||
узнаёт свою запись. Отсюда обязанность экрана — показывать его текстом, а не
|
||||
разметкой.
|
||||
|
||||
## Рамки
|
||||
|
||||
Поиска по тексту, переименования и удаления записей не делаем. Правки текста не
|
||||
делаем — это граница из [паспорта](../../docs/passport.md): расшифровку отдаём как
|
||||
есть.
|
||||
Поиска по тексту не делаем. Переименование записи, возврат её в работу и журнал
|
||||
событий — `audiorecord-actions`; удаление — `delete-record`, проигрывание —
|
||||
`play-recording-in-app`. Правки текста
|
||||
не делаем — это граница из [паспорта](../../docs/passport.md): расшифровку отдаём
|
||||
как есть.
|
||||
|
||||
@@ -12,18 +12,19 @@
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- `TranscribeService.createTranscribeJob` — проверка длительности и веса до
|
||||
записи файла на диск;
|
||||
- контракт `POST /api/audio`: код и тело ответа на запись сверх потолка;
|
||||
- ответ бота на слишком длинную запись;
|
||||
- `TranscribeService.createRecord` — проверка длительности и веса до записи
|
||||
файла в хранилище;
|
||||
- контракт `POST /app/audiorecords`: код и тело ответа на запись сверх потолка;
|
||||
- `GET /app/config` — потолок длительности рядом с потолком размера, чтобы экран
|
||||
отклонял такую запись до отправки;
|
||||
- секция конфигурации: потолок длительности и потолок веса;
|
||||
- `docs/database.md` — настройки с числовым значением.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Запись длиннее потолка отклоняется на приёме: задача не заводится, файл на
|
||||
диске не остаётся. Оракул — тест приёма на записи сверх потолка: задач ноль,
|
||||
каталог хранения пуст.
|
||||
- Запись длиннее потолка отклоняется на приёме: аудиозапись не заводится, файл в
|
||||
хранилище не остаётся. Оракул — тест приёма на записи сверх потолка: записей
|
||||
ноль, каталог хранения пуст.
|
||||
- Отправитель видит человекочитаемый текст с названным пределом, а не код
|
||||
ошибки. Оракул — тест: в теле ответа предел числом, текста внутренней ошибки
|
||||
нет.
|
||||
@@ -36,5 +37,6 @@
|
||||
|
||||
Берётся после `intake-limits-measure`: до неё потолок брать неоткуда. Длительность
|
||||
известна из `ffprobe` — значит, проверка идёт после чтения метаданных, но до
|
||||
записи в базу. Предел веса на стороне сервера уже есть
|
||||
(`router.MaxMultipartMemory`), и он не отменяется, а дополняется.
|
||||
записи в базу. Предел размера на стороне сервера уже есть — `entity.MaxRecordSize`
|
||||
стоит и у поля файла, и у тела запроса приёма, — и он не отменяется, а
|
||||
дополняется потолком длительности.
|
||||
|
||||
@@ -22,14 +22,19 @@
|
||||
|
||||
```
|
||||
/auth/login → 302 ... http.route=/auth/login http.status_code=302
|
||||
/api/audio → 404 ... http.route=/api/audio http.status_code=0
|
||||
/app/audiorecords → 404 ... http.route=/app/audiorecords http.status_code=0
|
||||
/nope → 404 ... http.route=/nope http.status_code=0
|
||||
POST /api/audio без сессии → 401 ... http.status_code=0
|
||||
POST /app/audiorecords без сессии → 401 ... http.status_code=0
|
||||
```
|
||||
|
||||
Первый обработчик отвечает сам и код пишет верно; три остальных отвергнуты
|
||||
роутером и дают ноль.
|
||||
|
||||
Прогон снят до того, как приложение переехало из `/api/` в `/app/` решением
|
||||
владельца 2026-08-15: тогда те же строки стояли с адресом `/api/audio`. Предмет
|
||||
находки от переезда не зависит — ноль пишет роутер, а не обработчик, — и
|
||||
воспроизводится она с любым адресом, который роутер отверг.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- слой журнала запроса в `main.go`: место, где снимается код ответа;
|
||||
|
||||
+10
-10
@@ -1,13 +1,12 @@
|
||||
# ✨ Собрать каркас приложения и раздать его из бинарника
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь — Каркас приложения: экранов нет и собирать их нечем, а на экране стоят настройки, удаление, уровни текста и статистика.
|
||||
- **Категория:** Очередь — Каркас идёт сразу за контрактом: на нём стоят все экраны, а сам он ждёт готовой формы ответов
|
||||
- **Зачем:** Экранов нет и собирать их нечем: ни сборки фронтенда, ни раздачи статики в проекте не существует.
|
||||
|
||||
Каркас готовит четыре экрана — загрузку, состояние задачи, чтение текста и
|
||||
список, — но сам по себе пользователю ничего не даёт. Видимое от него одно:
|
||||
приложение открывается и достаёт данные с живого сервера, а не отдаёт пустую
|
||||
страницу.
|
||||
Каркас готовит экраны — загрузку, рубеж записи, чтение текста и список, — но сам
|
||||
по себе пользователю ничего не даёт. Видимое от него одно: приложение
|
||||
открывается и достаёт данные с живого сервера, а не отдаёт пустую страницу.
|
||||
|
||||
Фреймворк выбран 2026-08-11 — Vue 3 с роутером пятой версии и сборкой Vite
|
||||
([ADR](../../docs/adr/ADR-2026-08-11-spa-on-vue.md)); правила кода приложения
|
||||
@@ -16,8 +15,9 @@
|
||||
## Затрагивает
|
||||
|
||||
- новый каталог фронтенда: исходники, зависимости, конфигурация сборки;
|
||||
- роут, отдающий `index.html` на неизвестный путь вне `/api/`: адреса маршрутов
|
||||
обычные, а не после решётки;
|
||||
- роут, отдающий `index.html` на неизвестный путь вне корней сервиса — `/api/`
|
||||
у хранилища, `/app/` у приложения, `/auth/` у входа, `/_/` у панели, плюс
|
||||
`/health` и `/metrics`: адреса маршрутов обычные, а не после решётки;
|
||||
- `Taskfile.yml` — шаг сборки статики и его место в `task gate` и `task image`;
|
||||
- `Dockerfile` — сборка статики внутри образа, чтобы выкладка не зависела от
|
||||
машины разработчика;
|
||||
@@ -38,9 +38,9 @@
|
||||
- Шаг сборки статики входит в `task gate` и краснеет при ошибке сборки. Оракул —
|
||||
намеренно сломанный исходник роняет `task gate`.
|
||||
- Обновление страницы на любом маршруте приложения открывает тот же экран, а
|
||||
адрес внутри `/api/` в приложение не проваливается. Оракул — тест на два
|
||||
запроса: неизвестный путь вне `/api/` отдаёт разметку, неизвестный путь внутри
|
||||
`/api/` отдаёт ошибку контракта.
|
||||
адрес внутри корня сервиса в приложение не проваливается. Оракул — тест на три
|
||||
запроса: неизвестный путь вне корней отдаёт разметку, неизвестный путь внутри
|
||||
`/api/` и внутри `/app/` отдаёт ошибку контракта.
|
||||
|
||||
## Рамки
|
||||
|
||||
|
||||
@@ -1,36 +1,39 @@
|
||||
# ✨ Сделать экран загрузки записи и её состояния
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь — Первое, ради чего приложение открывают; требует каркаса и контракта, оба выше.
|
||||
- **Категория:** Очередь — Первое, ради чего приложение открывают; требует каркаса и контракта, оба выше
|
||||
- **Зачем:** Первое, ради чего приложение открывают: отдать файл и увидеть, что с ним происходит.
|
||||
|
||||
Экран принимает файл и заводит задачу, а её состояние обновляется само, пока
|
||||
задача не дошла до `done` или `failed`.
|
||||
Экран принимает файл и заводит запись, а её рубеж обновляется сам, пока запись
|
||||
не дошла до конечного рубежа `done` либо не встала признаком остановки.
|
||||
|
||||
Берётся после `spa-skeleton` и `json-api-for-spa`.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- новый экран приложения: выбор файла, отправка, показ состояния;
|
||||
- опрос состояния задачи и его остановка;
|
||||
- новый экран приложения: выбор файла, отправка, показ рубежа;
|
||||
- опрос рубежа записи и его остановка;
|
||||
- показ отказа человекочитаемым текстом из контракта API;
|
||||
- предел размера загружаемого файла на стороне приложения и на стороне сервера
|
||||
(`router.MaxMultipartMemory`, сегодня 32 МиБ);
|
||||
- предел размера записи на стороне приложения: экран берёт его из
|
||||
`GET /app/config`, где сервер отдаёт своё же число;
|
||||
- `docs/database.md` — частота опроса числом.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Выбранный файл уходит на сервер и заводит задачу; экран сразу показывает её
|
||||
состояние. Оракул — тест экрана на подставном API: после отправки на экране
|
||||
идентификатор задачи и состояние `created`.
|
||||
- Опрос сам прекращается, когда задача дошла до `done` или `failed`. Оракул —
|
||||
тест: после ответа `done` новых запросов к API нет.
|
||||
- Отказ задачи показывается человекочитаемым текстом, а не кодом и не сырой
|
||||
ошибкой. Оракул — тест на ответе с состоянием `failed`.
|
||||
- Выбранный файл уходит на сервер и заводит запись; экран сразу показывает её
|
||||
рубеж. Оракул — тест экрана на подставном API: после отправки на экране
|
||||
идентификатор записи и рубеж `uploaded`.
|
||||
- Опрос сам прекращается, когда запись дошла до `done` либо встала признаком
|
||||
остановки. Оракул — два теста: после ответа с рубежом `done` новых запросов к
|
||||
API нет; после ответа с признаком остановки — тоже.
|
||||
- Остановленная запись показывается человекочитаемым текстом, а не кодом и не
|
||||
сырой ошибкой; машинного текста отказа контракт не отдаёт вовсе, поэтому текст
|
||||
строится из причины остановки. Оракул — тест на ответе с признаком остановки и
|
||||
причиной.
|
||||
- Файл больше предела отклоняется на экране до отправки, с названным числом, и
|
||||
предел экран берёт у сервера, а не держит своей константой. Оракул — тест на
|
||||
файле сверх предела: запроса на загрузку нет, на экране предел числом, и
|
||||
смена предела на стороне сервера меняет это число.
|
||||
предел экран берёт из `GET /app/config`, а не держит своей константой. Оракул
|
||||
— тест на файле сверх предела: запроса на загрузку нет, на экране предел
|
||||
числом, и другое число в ответе `config` меняет показанное.
|
||||
|
||||
## Рамки
|
||||
|
||||
|
||||
Reference in New Issue
Block a user