закрыта задача json-api-for-spa
This commit is contained in:
@@ -43,7 +43,6 @@
|
||||
|
||||
## Очередь
|
||||
|
||||
- [✨ Свести приём и чтение записей к одному контракту для приложения](items/json-api-for-spa.md) — Сегодняшний API отвечает 404 на любую ошибку чтения и 500 на любую ошибку приёма: строить на нём экраны нечем.
|
||||
- [✨ Собрать каркас приложения и раздать его из бинарника](items/spa-skeleton.md) — Экранов нет и собирать их нечем: ни сборки фронтенда, ни раздачи статики в проекте не существует.
|
||||
- [✨ Сделать экран загрузки записи и её состояния](items/upload-and-status-screen.md) — Первое, ради чего приложение открывают: отдать файл и увидеть, что с ним происходит.
|
||||
- [✨ Сделать экран списка своих записей и чтения текста](items/records-list-screen.md) — Расшифровка сегодня доходит одним сообщением и теряется в переписке; вернуться к ней через неделю нечем.
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
# ✨ Свести приём и чтение записей к одному контракту для приложения
|
||||
|
||||
- **Тип:** feature
|
||||
- **Категория:** Очередь — Проект экранов и адресов согласован с владельцем 2026-08-15: на этом контракте стоят каркас приложения и все его экраны, а стоящие выше правки входа и хранилища его не подпирают
|
||||
- **Зачем:** Сегодняшний API отвечает 404 на любую ошибку чтения и 500 на любую ошибку приёма: строить на нём экраны нечем.
|
||||
|
||||
Экраны заводят задачу, видят её состояние и листают список — всё через один
|
||||
контракт.
|
||||
|
||||
Обработчик `GET /api/status/:id` сегодня отвечает `404` на **любую** ошибку
|
||||
чтения, включая сбой базы, а `POST /api/audio` — `500` на любую ошибку заведения,
|
||||
включая негодный файл. Экран, построенный на таком контракте, показывает «не
|
||||
найдено» при упавшей базе.
|
||||
|
||||
Отдельная ветка того же класса найдена ревью задачи `remove-telegram-intake`
|
||||
2026-08-15, проход сверки требований: приём отвергает пустого владельца своей
|
||||
ошибкой, а транспорт переводит **любую** ошибку службы в `500`. Спека приёма при
|
||||
этом держит эту проверку ради понятного отказа — «отвечает отправителю понятным
|
||||
отказом до того, как запись попадёт в память». Сегодня путь недостижим: слой
|
||||
предъявления отвергает такого вызывающего раньше. Он становится достижимым вместе
|
||||
с личными токенами и экранами приложения, то есть ровно здесь.
|
||||
|
||||
**Состав контракта владелец согласовал 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 /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/controller/http` целиком;
|
||||
- спека `intake` — новый адрес приёма, убранный опрос готовности и суженная
|
||||
норма про имя отправителя;
|
||||
- спека `access` — область слоя предъявления сессии названа там адресами
|
||||
приложения;
|
||||
- `docs/conventions/web-ui.md` — правило неизвестного пути перечисляет корни
|
||||
сервиса, а не один `/api/`;
|
||||
- `docs/architecture.md`, раздел «Единые точки проекта»;
|
||||
- `docs/database.md` — новые колонки записи.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
- Код ответа отвечает причине отказа, а не месту, где он случился: сбой базы при
|
||||
чтении даёт `500`, а не `404`, негодный файл — `400` с человекочитаемым
|
||||
текстом, а не `500`, а отказ приёма по пустому владельцу — `403`, как и отказ
|
||||
слоя предъявления. Тело ошибки при этом одной формы на всех адресах, без
|
||||
сырого `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`.
|
||||
|
||||
## Рамки
|
||||
|
||||
Аутентификацию и владельца не заводим — это `oidc-login` и `record-ownership`;
|
||||
задача про форму контракта. Публичный контракт после мерджа обратной правкой не
|
||||
откатывается, и по решению владельца от 2026-08-12 форма ответа приёма
|
||||
согласуется здесь один раз — сразу списком и с местом под признак повтора, —
|
||||
чтобы `dedup-by-content-hash`, `multi-file-upload` и `reject-oversized-recording`
|
||||
её не переписывали, а экран загрузки не переделывался под вторую форму. Приём
|
||||
по-прежнему берёт из запроса один файл: меняется форма ответа, не число файлов.
|
||||
Сюда же приехала опечатка `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`. Имени файла отправителя он при этом не трогает — колонки
|
||||
разные, и вопроса «что затрёт что» у них не возникает. Правки заголовка руками в
|
||||
приложении здесь тоже нет: колонка под неё заведена, а экран и адрес принесёт
|
||||
задача, которой это понадобится.
|
||||
Reference in New Issue
Block a user