# Tasks — Profile picture upload

> Red/Green TDD order: each implementation task is preceded by the failing test that
> requires it. Task IDs are referenced from `spec.md` → Traceability and from `.specify/rtm.md`.
> Check off as work lands; reference the issue in each commit (`Refs #<n>`).

## Backend

- [ ] **T-1** Add `ErrorCode.AVATAR_TOO_LARGE` in all four sites at once: `ErrorCode.java`,
      `frontend/src/lib/shared/errors.ts`, `getErrorMessage()`, `messages/{de,en,es}.json`.
      *(No new behavior yet — enables REQ-008's error.)* → covers REQ-008 (error plumbing)
- [ ] **T-2** `@WebMvcTest` `UserAvatarControllerTest`: write failing slice tests —
      `unauthenticatedReturns401`, `rejectsNonImage` (400 UNSUPPORTED_FILE_TYPE),
      `rejectsOversize` (400 AVATAR_TOO_LARGE). Then implement `UserAvatarController` +
      `@RequirePermission` to green. → REQ-006, REQ-007, REQ-008
- [ ] **T-3** Unit `UserServiceAvatarTest`: failing tests `storesUnderUserKey`,
      `replaceLeavesNoOrphan`, validation maps to `DomainException`. Then implement
      `UserService.setAvatar`/`removeAvatar` (mock `FileService`) to green. → REQ-001, REQ-002, REQ-003
- [ ] **T-4** Flyway `V78__add_app_user_avatar_object_key.sql` (verify next free number on
      disk) adding nullable `avatar_object_key VARCHAR(512)`; add the column + `@Schema` to
      `AppUser` / `UserProfileView` (`avatarUrl` derived). Test: repository round-trip. → REQ-002
- [ ] **T-5** `deleteMyAvatar` controller test + impl (clears key, deletes object, returns
      `avatarUrl: null`). → REQ-003
- [ ] **T-6** Admin path: failing tests `adminDeletesOthersAvatar` (200),
      `nonAdminForbiddenOnOthers` (403). Implement ownership/`ADMIN_USER` check to green. → REQ-005, REQ-009
- [ ] **T-7** Authenticated proxy `getUserAvatar` streaming endpoint + `Content-Type` +
      `X-Content-Type-Options: nosniff`; test 200 bytes / 404 when no avatar. → REQ-004 (view side)
- [ ] **T-A** Run `npm run generate:api` after T-4/T-7 so `avatarUrl` lands in `api.ts`.

## Frontend

- [ ] **T-8** i18n keys for the new strings in `messages/{de,en,es}.json` (button labels,
      validation errors mapped via `getErrorMessage`). → REQ-007, REQ-008 (UX)
- [ ] **T-9** Component test `avatar-placeholder.svelte.spec.ts`: failing test asserting
      initials render when `avatarUrl` is null; implement the placeholder. → REQ-004
- [ ] **T-10** `/profile` upload control: file picker, client-side type/size pre-check,
      instant preview, confirm/remove. States: idle/preview/uploading/error/done. → REQ-002, REQ-003
- [ ] **T-11** Render avatar where names appear (comments, activity feed, `/users/[id]`),
      falling back to the placeholder. → REQ-004
- [ ] **T-12** E2E `avatar.spec.ts`: upload → preview → confirm → avatar visible; remove →
      initials return. → REQ-002, REQ-003, REQ-004

## Cross-cutting

- [ ] **T-13** Set `spring.servlet.multipart.max-file-size` to a 2 MB-matching ceiling so an
      oversized body is rejected at the container edge (defense in depth for REQ-008).
- [ ] **T-14** Update `.specify/rtm.md` Status column to `Done` per REQ as each test goes green.
