diff --git a/tasks/BACKLOG.md b/tasks/BACKLOG.md index 5aec6f3..ca4355b 100644 --- a/tasks/BACKLOG.md +++ b/tasks/BACKLOG.md @@ -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) — Расшифровка сегодня доходит одним сообщением и теряется в переписке; вернуться к ней через неделю нечем. diff --git a/tasks/items/json-api-for-spa.md b/tasks/items/json-api-for-spa.md deleted file mode 100644 index 31390a9..0000000 --- a/tasks/items/json-api-for-spa.md +++ /dev/null @@ -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`. Имени файла отправителя он при этом не трогает — колонки -разные, и вопроса «что затрёт что» у них не возникает. Правки заголовка руками в -приложении здесь тоже нет: колонка под неё заведена, а экран и адрес принесёт -задача, которой это понадобится.