chore(openspec): archive add-user-avatar and sync main specs
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-12
|
||||
@@ -0,0 +1,119 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
См. `proposal.md` — Why. Текущее состояние, определяющее подход:
|
||||
|
||||
- `users.avatar_url` (text) существует в Postgres и SQLite с миграции `000001`; поля
|
||||
`User.AvatarURL` / `PublicUser.AvatarURL` / `SelfUser.AvatarURL` и `avatarUrl` в обоих
|
||||
клиентах уже объявлены, но никто их не пишет и не отдаёт.
|
||||
- Объектное хранилище фото-центрично: журнал `objects` (pending/ready/deleting) плюс
|
||||
`photo_objects(photo_id, object_id, role)`; URL строится через `storage.URL(key)` как
|
||||
подписанная ссылка `/api/objects/<key>?e=&s`.
|
||||
- Сборка мусора (`ClaimGarbage`, Postgres и SQLite) удаляет `ready`-объекты **без привязки** в
|
||||
`photo_objects`. Значит объект аватара обязан иметь привязку, иначе его подоберёт GC.
|
||||
- `cmd/reconcile` сканирует и удаляет сирот только под префиксом `objects/`; ключи аватара под
|
||||
этим префиксом попадут в журнал и будут безопасны.
|
||||
- Обработка изображений: `disintegration/imaging` (`imaging.Fill` — квадратный кроп) и WebP.
|
||||
- Android-кэш Coil (`ObjectCacheKey`) ключует по пути, **отбрасывая query**, чтобы переживать
|
||||
ротацию подписи у фото. Это влияет на форму URL аватара.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Круглый аватар с загрузкой и кадрированием в вебе и Android; отображение везде, где виден пользователь.
|
||||
- Хранение аватара, безопасное относительно существующего GC и `reconcile`.
|
||||
- Минимум изменений в коде чтения пользователей.
|
||||
|
||||
**Non-Goals:**
|
||||
- Серверное кадрирование по геометрии (кадр делает клиент).
|
||||
- Аватары на публичных share-страницах (там сейчас нет авторов в UI).
|
||||
- Хранение исходника загруженного фото как оригинального объекта аватара.
|
||||
- Анимированные аватары (GIF/видео).
|
||||
|
||||
## Decisions
|
||||
|
||||
### Кадрирование на клиенте, сервер нормализует
|
||||
Клиент (веб — canvas, Android — Compose) отдаёт уже готовый квадрат; сервер не принимает
|
||||
геометрию и не доверяет ей. Причины: у каждой платформы есть естественный кроп-UI; серверное
|
||||
API остаётся «тупым» (один файл), не нужно валидировать прямоугольник/поворот.
|
||||
*Альтернатива:* серверный кроп по `x,y,w,h,angle` — один алгоритм, но API сложнее, а кроп-UI всё
|
||||
равно нужен на клиенте. Отклонено.
|
||||
*Реализация UI:* без новых зависимостей — собственный canvas в вебе и Compose-кроп в Android.
|
||||
*Альтернатива:* сторонние библиотеки кропа; допустимо, но добавляет зависимости.
|
||||
|
||||
### Новый эндпоинт с одним файлом
|
||||
`POST /api/auth/me/avatar` (multipart `file` = квадрат) и `DELETE /api/auth/me/avatar`. Ответ —
|
||||
обновлённый self-профиль (как у прочих изменений профиля).
|
||||
|
||||
### Нормализация: две квадратные нарезки, WebP, без EXIF
|
||||
Сервер декодирует, отклоняет не-изображения, делает `imaging.Fill` в 512 и 128 и перекодирует в
|
||||
WebP. Повторный энкод снимает EXIF/GPS. Роли нарезок: `avatar` (512) и `avatar_small` (128).
|
||||
*Альтернатива:* одна нарезка 512 — меньше объектов, но лишний трафик в списках. Отклонено.
|
||||
*Альтернатива:* хранить оригинал аватара как object — не нужно, аватар не «открывают».
|
||||
|
||||
### Привязка через `user_avatar_objects` по образцу `photo_objects`
|
||||
Новая таблица `user_avatar_objects(user_id, object_id, role)`. GC получает второе исключение: не
|
||||
удалять объекты, на которые ссылается ни фото, ни аватар. Это сохраняет инвариант «журнал
|
||||
объектов — источник правды», делает `reconcile` безопасным и **автоматически** освобождает
|
||||
старый блоб при замене/удалении аватара: старая привязка снимается, объект теряет ссылку и его
|
||||
подбирает существующий воркер.
|
||||
*Альтернатива:* отдельный префикс (`avatars/`) вне журнала — проще, но выпадает из журнала,
|
||||
лишается управления жизненным циклом и нарушает принцип из `media-objects`. Отклонено.
|
||||
*Альтернатива:* хранить аватар как фото без альбома (`album_id` nullable уже есть) — переиспользует
|
||||
`photo_objects` бесплатно, но засоряет фото-представления (корзина, теги). Отклонено.
|
||||
|
||||
### Стабильный URL с версией в пути
|
||||
`GET /api/avatars/:userID/:version` отдаёт малую нарезку, `?size=large` — крупную; `:version` —
|
||||
идентификатор текущего объекта аватара, сегмент нужен только для инвалидации кэша.
|
||||
`users.avatar_url` заполняется значением `/api/avatars/<userID>/<version>`.
|
||||
Чтение **публичное** (без авторизации): теги `<img>` и клиентские загрузчики изображений
|
||||
(Coil) не могут отправить заголовок `Authorization`, поэтому защищённый маршрут отдавал бы
|
||||
401 и «битую» картинку. Загрузка и удаление остаются под авторизацией. Путь не угадывается
|
||||
по одному идентификатору: `userID` и `version` — непредсказуемые UUID.
|
||||
Причины: существующие запросы (`members`, `social`, `photos`, `auth/me`) уже выбирают
|
||||
`avatar_url`, поэтому «аватар везде» не требует правок чтения; версия **в пути**, а не в query,
|
||||
потому что Coil ключует кэш по пути (query отбрасывается) — иначе после замены отдавалась бы
|
||||
старая картинка.
|
||||
*Альтернатива:* подписанные `/api/objects?e=&s` как у фото — работает в `<img>`, но подпись
|
||||
пришлось бы резолвить в четырёх сервисах на каждый ответ, а ссылка бы истекала (нельзя хранить
|
||||
в колонке). Отклонено.
|
||||
*Альтернатива:* версия в query `?v=` — ломает кэш Coil без правки `ObjectCacheKey`. Отклонено.
|
||||
|
||||
### Заглушка и удаление
|
||||
При отсутствии аватара — нейтральный силуэт: в веб-спрайт добавляется иконка `person`, в Android
|
||||
используется `Icons.Filled.Person`. `DELETE /api/auth/me/avatar` снимает привязку и очищает
|
||||
`avatar_url`.
|
||||
|
||||
### Лимит размера
|
||||
Отдельный небольшой предел на файл аватара (константа 10 MB), а не общий `IMAGE_MAX_UPLOAD_SIZE`
|
||||
(100 MB): аватар такого размера не нужен, а ранний отказ экономит ресурсы.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [GC-запрос меняется в двух сторах, легко забыть SQLite] → задача явно требует правку обоих
|
||||
бэкендов и тест на освобождение объекта аватара.
|
||||
- [Денормализация `avatar_url` может разойтись с фактической привязкой] → URL строится из
|
||||
идентификатора объекта в момент привязки; замена всегда переписывает оба.
|
||||
- [Клиент может прислать не-квадрат] → сервер делает `imaging.Fill` — результат всегда квадрат
|
||||
независимо от входа.
|
||||
- [Coil: смена версии в пути меняет ключ кэша → старая запись остаётся на диске] → приемлемо,
|
||||
Coil вытеснит её по LRU; корректность важнее.
|
||||
- [Кроп-UI на двух платформах — дублирование усилий] → умышленно: каждый UI нативен платформе.
|
||||
- [Учётные записи без `avatar_url`] → заглушка во всех точках отображения; значение `null`/пусто.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Миграция Postgres `000014_user_avatar_objects.up.sql` и SQLite `000006_user_avatar_objects.up.sql`:
|
||||
создать `user_avatar_objects` с FK на `users` и `objects` (каскад/`SET NULL` по обстоятельствам),
|
||||
уникальность `(user_id, role)`.
|
||||
2. Правка GC (`ClaimGarbage`) в Postgres и SQLite + сборка мусора теперь учитывает аватар.
|
||||
3. Бэкенд: сервис/хендлеры аватара, отдача `/api/avatars/...`, заполнение `avatar_url`, маршруты.
|
||||
4. Клиенты: веб и Android.
|
||||
5. Откат: удалить маршруты и таблицу; `avatar_url` очищается, объекты аватара становятся
|
||||
недоступными и освобождаются GC. Существующие данные не портятся.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Точное значение предела размера (сейчас 10 MB) и набор принимаемых форматов (по факту — то, что
|
||||
отдаёт canvas/Android: JPEG/PNG/WebP) можно уточнить при реализации, не меняя подход.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
Аватар профиля задуман, но не реализован: колонка `users.avatar_url`, поля
|
||||
`PublicUser.AvatarURL`/`SelfUser.AvatarURL` и `avatarUrl` в клиентах существуют, однако
|
||||
никто их не пишет, изображение никто не отдаёт, а интерфейс везде показывает только имена.
|
||||
Пользователи не могут выделить себя визуально. Нужно замкнуть эту заготовку в рабочую
|
||||
круглую фотографию профиля с загрузкой и кадрированием на обеих платформах.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Загрузка и удаление аватара: `POST /api/auth/me/avatar` (клиент присылает уже готовый
|
||||
квадрат после кадрирования) и `DELETE /api/auth/me/avatar` (сброс к дефолту).
|
||||
- Серверная нормализация загруженного изображения: отклонение не-изображений, снятие
|
||||
EXIF/GPS повторным энкодом, две квадратные WebP-нарезки — `avatar_small` (128) и
|
||||
`avatar` (512).
|
||||
- Аватар хранится как объект журнала, привязанный к пользователю; сборка мусора учитывает
|
||||
эту привязку, поэтому замена и сброс аватара сами освобождают старый блоб.
|
||||
- Стабильная отдача под авторизацией: `GET /api/avatars/:userID/:version` (версия в пути,
|
||||
чтобы клиентские и браузерные кэши инвалидировались при замене), `?size=large` отдаёт
|
||||
крупную нарезку.
|
||||
- `users.avatar_url` заполняется стабильной ссылкой, поэтому существующие публичные
|
||||
представления пользователя (участники альбома, комментарии, авторы) начинают отдавать
|
||||
аватар без изменения своих запросов.
|
||||
- Веб-интерфейс: круглый аватар и загрузка с кадрированием на странице профиля; круглый
|
||||
аватар в шапке, у участников, в комментариях и у автора фото.
|
||||
- Android-клиент: круглый аватар и загрузка с кадрированием на экране профиля; круглый
|
||||
аватар в шапке списка альбомов.
|
||||
- Дефолт при отсутствии аватара — нейтральная иконка-силуэт; в веб-спрайт добавляется иконка
|
||||
`person`.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
<!-- None: аватар относится к существующей способности профиля пользователя. -->
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `user-profile`: добавляются требования к аватару — загрузка с кадрированием на клиенте,
|
||||
круглое отображение в вебе (шапка, страница профиля, участники, комментарии, автор) и в
|
||||
Android (экран профиля, шапка списка альбомов), дефолтная заглушка и удаление аватара.
|
||||
- `media-objects`: пользовательский аватар становится объектом журнала, привязанным к
|
||||
пользователю, и учитывается сборкой мусора наряду с привязками к фото.
|
||||
|
||||
## Impact
|
||||
|
||||
- Backend: `internal/auth` (хендлеры/сервис/сторы), новая привязка объектов аватара и её учёт
|
||||
в сборке мусора (`internal/photos/objects.go`, `sqlite_objects.go` или выделенный слой),
|
||||
маршрутизация и отдача (`internal/app`), миграции Postgres и SQLite, журнал объектов.
|
||||
- Frontend: `lib/api.ts`, `lib/stores/auth.svelte.ts`, `lib/icons/registry.ts`, страница
|
||||
`routes/(app)/profile`, `AppShell.svelte`, `Comments.svelte`, `PhotoPanel.svelte`, экран
|
||||
участников.
|
||||
- Android: `ProfileScreen`/`ProfileViewModel`/`ProfileApi`/`GaloApi`, `AlbumsScreen`,
|
||||
экран(ы) кадрирования; `UserDto.avatarUrl` уже есть.
|
||||
- API: новые маршруты `/auth/me/avatar` и `/avatars/:userID/:version`.
|
||||
- Спеки: `user-profile`, `media-objects`.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Spec Delta
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Сборка мусора по ссылкам
|
||||
Объект хранилища SHALL удаляться только когда на него не ссылается ни одно фото и ни один
|
||||
аватар пользователя; удаление альбома и попадание фото в корзину SHALL сохранять объекты.
|
||||
|
||||
#### Scenario: Последняя ссылка удалена
|
||||
- **WHEN** фото удаляется навсегда и больше ни одно фото не ссылается на его объекты
|
||||
- **THEN** объекты удаляются из хранилища
|
||||
|
||||
#### Scenario: Остались ссылки
|
||||
- **WHEN** на объект ссылается другое фото
|
||||
- **THEN** объект сохраняется в хранилище
|
||||
|
||||
#### Scenario: Удаление альбома
|
||||
- **WHEN** владелец удаляет альбом
|
||||
- **THEN** объекты хранилища не удаляются
|
||||
|
||||
#### Scenario: Ссылка аватара
|
||||
- **WHEN** на объект ссылается аватар пользователя
|
||||
- **THEN** объект сохраняется в хранилище, пока ссылка существует
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Аватар пользователя как объект журнала
|
||||
Аватар пользователя SHALL храниться как объект журнала, привязанный к пользователю по ролям,
|
||||
соответствующим нарезкам аватара. Замена и удаление аватара SHALL менять привязку, а объекты
|
||||
без ссылок SHALL освобождаться сборкой мусора.
|
||||
|
||||
#### Scenario: Привязка аватара
|
||||
- **WHEN** пользователь устанавливает аватар
|
||||
- **THEN** объекты нарезок привязываются к пользователю по своим ролям
|
||||
|
||||
#### Scenario: Замена аватара
|
||||
- **WHEN** пользователь заменяет аватар
|
||||
- **THEN** привязка указывает на новые объекты, а старые освобождаются сборкой мусора
|
||||
|
||||
#### Scenario: Удаление аватара
|
||||
- **WHEN** пользователь удаляет аватар
|
||||
- **THEN** привязка снимается, и объекты аватара освобождаются сборкой мусора
|
||||
@@ -0,0 +1,89 @@
|
||||
# Spec Delta
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Загрузка фото профиля с выбором области
|
||||
Пользователь SHALL загружать собственное фото в профиль, выбирая видимую квадратную область.
|
||||
Сохранённый аватар SHALL соответствовать выбранной области. Загрузка SHALL быть доступна только
|
||||
владельцу профиля.
|
||||
|
||||
#### Scenario: Успешная загрузка
|
||||
- **WHEN** пользователь выбирает изображение и подтверждает квадратную область
|
||||
- **THEN** аватар обновляется и возвращается в профиле
|
||||
|
||||
#### Scenario: Выбранная область учтена
|
||||
- **WHEN** пользователь сдвигает или масштабирует область перед подтверждением
|
||||
- **THEN** сохранённый аватар показывает именно выбранную область
|
||||
|
||||
#### Scenario: Не изображение
|
||||
- **WHEN** загруженный файл не является декодируемым изображением
|
||||
- **THEN** система отклоняет запрос и не меняет аватар
|
||||
|
||||
### Requirement: Нормализация аватара и снятие метаданных
|
||||
Система SHALL сохранять аватар в виде квадратных изображений фиксированных размеров и SHALL NOT
|
||||
сохранять EXIF-метаданные загруженного файла.
|
||||
|
||||
#### Scenario: Метаданные не сохраняются
|
||||
- **WHEN** загружено изображение с EXIF, включая GPS-координаты
|
||||
- **THEN** сохранённый аватар не содержит EXIF и GPS
|
||||
|
||||
#### Scenario: Слишком большой файл
|
||||
- **WHEN** размер загруженного файла превышает допустимый предел
|
||||
- **THEN** система отклоняет запрос и не меняет аватар
|
||||
|
||||
### Requirement: Отдача аватара по стабильному URL
|
||||
Профиль и публичные представления пользователя SHALL содержать URL аватара, когда он задан.
|
||||
URL SHALL быть стабильным и SHALL изменяться при обновлении аватара, чтобы клиентские кэши
|
||||
инвалидировались. Изображение SHALL отдаваться по публичной ссылке без аутентификации, чтобы
|
||||
его могли загружать теги изображений и клиентские загрузчики.
|
||||
|
||||
#### Scenario: Ссылка в представлениях пользователя
|
||||
- **WHEN** аутентифицированный клиент запрашивает профиль, участников альбома, комментарии или автора фото
|
||||
- **THEN** представления содержат URL аватара, если он задан
|
||||
|
||||
#### Scenario: Кэш инвалидируется при замене
|
||||
- **WHEN** пользователь заменяет аватар
|
||||
- **THEN** URL аватара изменяется
|
||||
|
||||
#### Scenario: Загрузка изображения без заголовка авторизации
|
||||
- **WHEN** браузер или клиентский загрузчик изображений запрашивает изображение аватара по URL без заголовка авторизации
|
||||
- **THEN** система отдаёт изображение
|
||||
|
||||
### Requirement: Круглое отображение аватара в веб-интерфейсе
|
||||
Веб-приложение SHALL отображать аватар пользователя круглым в шапке, на странице профиля, у
|
||||
участников альбома, в комментариях и у автора фото.
|
||||
|
||||
#### Scenario: Аватар в шапке
|
||||
- **WHEN** аутентифицированный пользователь открывает любое место приложения
|
||||
- **THEN** в шапке отображается его круглый аватар
|
||||
|
||||
#### Scenario: Аватар рядом с другими пользователями
|
||||
- **WHEN** пользователь просматривает участников, комментарии или автора фото
|
||||
- **THEN** аватары этих пользователей отображаются круглыми
|
||||
|
||||
### Requirement: Круглое отображение аватара в Android-клиенте
|
||||
Android-приложение SHALL отображать аватар пользователя круглым на экране профиля и в шапке
|
||||
списка альбомов.
|
||||
|
||||
#### Scenario: Аватар на экране профиля
|
||||
- **WHEN** пользователь открывает экран профиля
|
||||
- **THEN** отображается его круглый аватар
|
||||
|
||||
#### Scenario: Аватар в шапке списка альбомов
|
||||
- **WHEN** пользователь открывает список альбомов
|
||||
- **THEN** в шапке отображается его круглый аватар
|
||||
|
||||
### Requirement: Заглушка при отсутствии аватара
|
||||
Когда аватар не задан, система SHALL показывать нейтральную заглушку-силуэт вместо изображения.
|
||||
|
||||
#### Scenario: Аватар не задан
|
||||
- **WHEN** у пользователя нет аватара и его представление отображается в вебе или Android
|
||||
- **THEN** вместо изображения показывается нейтральная круглая заглушка-силуэт
|
||||
|
||||
### Requirement: Удаление аватара
|
||||
Пользователь SHALL удалять свой аватар; после удаления SHALL показываться заглушка, а связанные
|
||||
объекты хранилища SHALL освобождаться.
|
||||
|
||||
#### Scenario: Сброс аватара
|
||||
- **WHEN** пользователь удаляет аватар
|
||||
- **THEN** представления возвращаются к заглушке, а объекты старого аватара освобождаются
|
||||
@@ -0,0 +1,47 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Модель данных и сборка мусора
|
||||
|
||||
- [x] 1.1 Добавить миграцию Postgres `000014_user_avatar_objects.up.sql`/`.down.sql` и SQLite `000006_user_avatar_objects.up.sql`/`.down.sql`: таблица `user_avatar_objects(user_id, object_id, role)` с FK на `users` и `objects` и уникальностью `(user_id, role)`; проверить, что тесты миграций (`go test ./internal/db/...`) проходят и `make migrate` применяет и откатывает.
|
||||
- [x] 1.2 Расширить `ClaimGarbage` в Postgres-сторе (`internal/photos/objects.go`): не считать мусором объекты, на которые ссылается `user_avatar_objects`; проверить тестом, что привязанный объект аватара переживает сборку, а после снятия привязки освобождается.
|
||||
- [x] 1.3 Расширить `ClaimGarbage` в SQLite-сторе (`internal/photos/sqlite_objects.go`) тем же исключением; проверить аналогичным тестом на SQLite.
|
||||
- [x] 1.4 Добавить методы стора для аватара (привязать/заменить/снять по ролям `avatar`/`avatar_small`, получить текущие объекты) на обоих бэкендах; проверить контрактным тестом, покрывающим оба бэкенда.
|
||||
|
||||
## 2. Backend: нормализация и жизненный цикл объекта аватара
|
||||
|
||||
- [x] 2.1 Реализовать нормализацию аватара: декодировать, отклонить не-изображение, `imaging.Fill` в 512 и 128, перекодировать в WebP (снятие EXIF/GPS); проверить юнит-тестом на JPEG с EXIF/GPS — результат квадратный 512 и 128 без EXIF.
|
||||
- [x] 2.2 Реализовать сервис установки/замены/удаления аватара через журнал объектов (pending → Put → ready → привязка, откат при ошибке); проверить тестом, что сбой не оставляет объектов, а успех привязывает обе роли.
|
||||
- [x] 2.3 Применить предел размера файла (10 MB) и отклонение не-изображений на границе сервиса; проверить тестами на превышение размера и на не-изображение.
|
||||
|
||||
## 3. Backend: API и отдача
|
||||
|
||||
- [x] 3.1 Добавить хендлеры `POST /api/auth/me/avatar` (multipart `file`) и `DELETE /api/auth/me/avatar`, возвращающие обновлённый self-профиль; проверить тестами: без аутентификации — отказ, после загрузки `avatar_url` задан, после удаления — пуст.
|
||||
- [x] 3.2 Добавить отдачу `GET /api/avatars/:userID/:version` (малая нарезка, `?size=large` — крупная) под авторизацией; проверить тестами: без аутентификации — отказ, версия меняется при замене, отсутствующий пользователь/аватар — корректная ошибка.
|
||||
- [x] 3.3 Зарегистрировать маршруты в `internal/app/app.go`; проверить тестом маршрутизации, что новые пути доступны и защищены middleware.
|
||||
|
||||
## 4. Аватар в представлениях пользователей
|
||||
|
||||
- [x] 4.1 Убедиться, что `auth/me`, участники, комментарии и автор фото отдают заполненный `avatar_url` (запросы уже выбирают колонку) и что `PublicUser`/`SelfUser` сериализуют поле; проверить тестами: у пользователя с аватаром `avatar_url` присутствует, без аватара — пуст.
|
||||
|
||||
## 5. Веб-интерфейс
|
||||
|
||||
- [x] 5.1 Добавить в `lib/api.ts` методы загрузки и удаления аватара и сохранять `avatar_url` в auth-стор; проверить `npm run check` (типы) без ошибок.
|
||||
- [x] 5.2 Добавить иконку `person` в `lib/icons/registry.ts` и компонент круглого аватара с заглушкой-силуэтом; проверить, что компонент показывает изображение при заданном URL и силуэт при пустом.
|
||||
- [x] 5.3 На странице `routes/(app)/profile` добавить выбор файла, канвас-кроппер и кнопки загрузки/удаления; проверить вручную: загрузка меняет аватар, удаление возвращает заглушку.
|
||||
- [x] 5.4 Показывать круглый аватар в `AppShell` (шапка), у участников, в `Comments.svelte` и у автора в `PhotoPanel.svelte`; проверить визуально, что заглушка и реальный аватар отображаются круглыми.
|
||||
|
||||
## 6. Android-клиент
|
||||
|
||||
- [x] 6.1 Добавить в `ProfileApi`/`GaloApi` загрузку (multipart) и удаление аватара, а `ProfileViewModel` — `avatarUrl` и действия; проверить юнит-тестами ViewModel (успех, ошибка, обновление состояния).
|
||||
- [x] 6.2 На `ProfileScreen` показать круглый аватар, выбор изображения и Compose-кроппер, кнопку удаления; проверить Compose-тестом и вручную, что аватар обновляется и сбрасывается.
|
||||
- [x] 6.3 Показать круглый аватар в шапке `AlbumsScreen`; проверить Compose-тестом, что при заданном `avatarUrl` рисуется изображение, иначе — силуэт.
|
||||
|
||||
## 7. Интеграция
|
||||
|
||||
- [x] 7.1 Прогнать сквозной сценарий: загрузка аватара отражается в шапке, у участников и в комментариях; замена освобождает старый объект (проверить сборкой мусора); удаление возвращает заглушку — тест проходит на обоих бэкендах метаданных.
|
||||
- [x] 7.2 Обновить документацию проекта (README/project.md), где перечислены эндпоинты и схема; проверить, что новые маршруты и таблица упомянуты.
|
||||
|
||||
## Workflow follow-up
|
||||
|
||||
- Архивировать изменение после прохождения ревью.
|
||||
- Проверить результат в архивированных спеках `user-profile` и `media-objects`.
|
||||
Reference in New Issue
Block a user