# Design ## Context See `proposal.md` - Why. Relevant current state: - `photos.Service.PermanentDelete` (backend/internal/photos/service.go:592) resolves authorization via `albums.Access(albumID, userID)` before `HardDelete`. A photo whose album was deleted has `album_id = NULL`; `Access("")` returns `album_not_found`, so the delete can never proceed. `Restore` already special-cases the missing album (`album_required`), but `PermanentDelete` does not. - The trash listing (`photos.Store.ListTrash`) is capped at `LIMIT 200` for display, while `TrashCount` is uncapped. A bulk purge must not silently stop at 200. - Object cleanup after `HardDelete` is per-photo: `PhotoObjectIDs` then `MarkObjectDeleting` for orphans, with a background worker removing blobs. - The trash screen (frontend/src/routes/(app)/trash/+page.svelte) has per-photo actions only, and `ConfirmDialog` is already available for confirmations. ## Goals / Non-Goals **Goals:** - Permanently deleting an albumless trashed photo works for its uploader. - One action empties the trash for everything the caller may delete, regardless of the 200-item display cap, and reports the number removed. - Consistent object garbage collection for every removed photo. **Non-Goals:** - Trash retention/auto-expiry policies. - Changing trash visibility rules (`ListTrash`) or the 200-item display cap. - Set-based SQL bulk deletion in this change. ## Decisions ### Authorize albumless permanent deletion by uploader When `p.AlbumID == ""`, `PermanentDelete` SHALL fall back to `p.UploaderID == userID` for authorization instead of calling `albums.Access`. When an album is present, the existing album-role check is unchanged. This mirrors `Restore`, which already treats a missing album as a distinct path, and keeps the permission model identical: only a photo's uploader can remove a photo that no longer belongs to an album. - Alternative considered: require the client to always supply a target album for delete. Rejected - permanent deletion has no target album, and the trash legitimately holds albumless photos. ### Empty trash as a server-side operation on the trash resource Add `DELETE /trash` returning `{ "deleted": }`, backed by `photos.Service.EmptyTrash(ctx, userID)`. It iterates the caller's accessible trashed photos and calls the existing `PermanentDelete` for each, counting successes and skipping photos the caller may not delete. Reusing `PermanentDelete` keeps authorization and object GC identical to single-photo deletion. - Alternative considered: client enumerates trash and calls `POST /photo-batch/...` with every id. Rejected - the display list is capped at 200 and this is racy; the server already knows the full accessible set. - Alternative considered: a single `DELETE FROM photos ... WHERE `. Rejected for now - album-role authorization lives in Go (`roles.CanDelete*`), and per-photo object GC is required anyway. ### Bypass the 200-item display cap for purging `EmptyTrash` must see more than the display page. Add a store method that returns all accessible trashed photo identities (id, album_id, uploader_id) with no display cap, and authorize each in the service. This avoids looping on a capped list (which would also risk an infinite loop when some items are not deletable). - Alternative considered: paginate `ListTrash` until a pass removes nothing. Rejected - undeletable items would reappear and a zero-progress pass is needed to stop; the uncapped identity listing is simpler and bounded by the user's own trash size. ### Frontend: explicit action plus confirmation Add an «Очистить корзину» control on the trash screen header, enabled when the list is non-empty. It opens the existing `ConfirmDialog`; on confirm it calls `DELETE /trash`, then reloads the list and refreshes `trash` store count. Show a busy state while running. ## Risks / Trade-offs - [Large trash makes the per-photo loop slow] -> Each `PermanentDelete` is a small fixed number of queries; bulk deletes are O(n) round trips. Acceptable for now; a set-based purge can replace the loop later without changing the spec behavior. Keep the frontend busy state to make latency legible. - [Uncapped listing memory] -> The query returns only the caller's trashed photo identities; bounded in practice by trash size. Revisit with chunking if it grows. - [Partial failure mid-purge] -> `EmptyTrash` skips failures and returns the count deleted; the remaining items stay in the trash and can be retried. - [Albumless deletes and realtime] -> `PermanentDelete` only publishes when `AlbumID != ""`; albumless removals publish nothing, which is correct since no album is watching. ## Migration Plan No schema change. Deploy backend and frontend together; the new endpoint is additive and the authorizaton fix is behavior-only. Rollback is reverting the deploy. ## Open Questions None blocking.