The cropper's onAvatarSaved/onBannerSaved callback used the url from
image.upload, which points at the temp buffer file. updateAvatar/updateBanner
then move that temp file to the NAS profile dir and delete the local copy, so
the url is dead the moment it's used — the avatar/banner rendered broken until
the next page load (the file itself was saved fine).
- updateAvatar/updateBanner now return the canonical media route URL
(route('media.avatar'|'media.banner', $relPath)), matching the model's
avatar_url/banner_url accessors and the project's no-asset('storage/...') rule.
- The image-cropper prefers the update endpoint's returned url over the temp
image.upload url; falls back to the old behavior when none is returned.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>