diff --git a/b0esche_cloud/lib/widgets/account_settings_dialog.dart b/b0esche_cloud/lib/widgets/account_settings_dialog.dart index 6827b31..6d998ae 100644 --- a/b0esche_cloud/lib/widgets/account_settings_dialog.dart +++ b/b0esche_cloud/lib/widgets/account_settings_dialog.dart @@ -113,6 +113,19 @@ class _AccountSettingsDialogState extends State { _avatarUploadProgress = 0.0; }); + // Immediately show a preview if the server returned the avatar bytes + try { + final avatarData = response['avatarData'] as String?; + final contentType = response['contentType'] as String?; + if (avatarData != null && contentType != null) { + setState(() { + _avatarUrl = 'data:$contentType;base64,$avatarData'; + }); + } + } catch (_) { + // ignore + } + // Fetch latest profile from backend so we get the canonical avatar URL and version try { final apiClient = GetIt.I(); diff --git a/go_cloud/internal/http/routes.go b/go_cloud/internal/http/routes.go index 0d71493..f8670b9 100644 --- a/go_cloud/internal/http/routes.go +++ b/go_cloud/internal/http/routes.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "database/sql" + "encoding/base64" "encoding/json" "fmt" "io" @@ -84,7 +85,11 @@ func writeAvatarCache(cfg *config.Config, userID string, version string, ext str dir = fallback } p := avatarCachePath(&config.Config{AvatarCacheDir: dir}, userID, version, ext) - return os.WriteFile(p, data, 0644) + if err := os.WriteFile(p, data, 0644); err != nil { + return err + } + fmt.Printf("[INFO] Wrote avatar cache for user=%s v=%s path=%s size=%d\n", userID, version, p, len(data)) + return nil } // readAvatarCache attempts to read a cached avatar for a user and optional version. If version is provided, it looks for an exact match; otherwise it returns the latest available cached avatar (if any). @@ -96,9 +101,12 @@ func readAvatarCache(cfg *config.Config, userID string, version string) ([]byte, // Also check fallback tmp dir checkDirs = append(checkDirs, filepath.Join(os.TempDir(), "b0esche_avatars")) + fmt.Printf("[DEBUG] readAvatarCache checking dirs=%v for user=%s version=%s\n", checkDirs, userID, version) + for _, dir := range checkDirs { entries, err := os.ReadDir(dir) if err != nil { + fmt.Printf("[DEBUG] readAvatarCache cannot read dir %s: %v\n", dir, err) continue } if version != "" { @@ -107,8 +115,10 @@ func readAvatarCache(cfg *config.Config, userID string, version string) ([]byte, for _, e := range entries { if strings.HasPrefix(e.Name(), prefix) { p := filepath.Join(dir, e.Name()) + fmt.Printf("[INFO] Found cached avatar for user=%s v=%s path=%s\n", userID, version, p) b, err := os.ReadFile(p) if err != nil { + fmt.Printf("[WARN] failed to read cached avatar %s: %v\n", p, err) return nil, "", err } ext := filepath.Ext(e.Name()) @@ -120,6 +130,7 @@ func readAvatarCache(cfg *config.Config, userID string, version string) ([]byte, } } // not found + fmt.Printf("[DEBUG] no exact cached avatar match in %s for prefix=%s\n", dir, prefix) continue } // no version specified: return latest by modtime of files starting with userID. @@ -139,8 +150,10 @@ func readAvatarCache(cfg *config.Config, userID string, version string) ([]byte, } if latest != nil { p := filepath.Join(dir, latestName) + fmt.Printf("[INFO] Found latest cached avatar for user=%s path=%s\n", userID, p) b, err := os.ReadFile(p) if err != nil { + fmt.Printf("[WARN] failed to read cached avatar %s: %v\n", p, err) return nil, "", err } ext := filepath.Ext(latestName) @@ -4213,8 +4226,11 @@ func uploadUserAvatarHandler(w http.ResponseWriter, r *http.Request, db *databas } // Save avatar bytes to local cache keyed by version so it survives restarts and avoids unnecessary re-downloads versionStr := fmt.Sprintf("%d", version) + cached := false if err := writeAvatarCache(cfg, userID.String(), versionStr, ext, fileBytes); err != nil { fmt.Printf("[WARN] failed to write avatar cache for user=%s version=%s: %v\n", userID.String(), versionStr, err) + } else { + cached = true } // Audit log @@ -4228,14 +4244,20 @@ func uploadUserAvatarHandler(w http.ResponseWriter, r *http.Request, db *databas }, }) + // Provide avatarData for immediate preview (small images only) + avatarData := base64.StdEncoding.EncodeToString(fileBytes) + publicURL := fmt.Sprintf("https://go.b0esche.cloud/user/avatar?v=%d", version) if token, ok := middleware.GetToken(r.Context()); ok && token != "" { publicURL = fmt.Sprintf("%s&token=%s", publicURL, token) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ - "message": "Avatar uploaded successfully", - "avatarUrl": publicURL, + "message": "Avatar uploaded successfully", + "avatarUrl": publicURL, + "cached": cached, + "avatarData": avatarData, + "contentType": contentType, }) }