From 2ab183596c688820f62f997e409d46762edad9f6 Mon Sep 17 00:00:00 2001 From: jules Date: Tue, 5 May 2026 09:56:40 +1000 Subject: [PATCH] profile: bootstrap avatar URL on app boot The appbar's reads avatarUrl from the localStorage profile mirror. Without a global fetcher, that mirror only got populated when the user navigated to /profile, so a fresh browser session showed initials in the appbar until then. - ProfileBootstrap component runs in root.tsx alongside LlmConfigBootstrap. On mount and on session change, fetches the arcadia profile and caches the resolved avatar URL. - profile.tsx loadAccount now also persists the URL into localStorage on initial fetch (was in-memory only) so it survives reloads. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/lib/profile-bootstrap.tsx | 47 +++++++++++++++++++++++++++++++++++ app/root.tsx | 2 ++ app/routes/profile.tsx | 12 ++++++++- 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 app/lib/profile-bootstrap.tsx diff --git a/app/lib/profile-bootstrap.tsx b/app/lib/profile-bootstrap.tsx new file mode 100644 index 0000000..323b8c1 --- /dev/null +++ b/app/lib/profile-bootstrap.tsx @@ -0,0 +1,47 @@ +// Fetches the arcadia profile on app boot (and after login) and caches +// the resolved avatar URL in localStorage so the appbar's shows +// immediately, without waiting for the user to navigate to /profile. + +import { useEffect } from "react" +import { useArcadiaClient } from "@crema/arcadia-client" + +import { getProfile, pickAvatarUrl } from "~/lib/arcadia/profiles" +import { loadProfile, saveProfile } from "~/lib/profile" + +export function ProfileBootstrap() { + const arcadia = useArcadiaClient() + + useEffect(() => { + let cancelled = false + + const tryBootstrap = async () => { + const token = + typeof window !== "undefined" + ? sessionStorage.getItem("arcadia_access_token") + : null + if (!token) return + try { + const p = await getProfile(arcadia) + if (cancelled) return + const url = pickAvatarUrl(p) + if (!url) return + const current = loadProfile() + if (current.avatarUrl === url) return + saveProfile({ ...current, avatarUrl: url }) + } catch { + // 401 / network — silently skip; will retry on next session change. + } + } + + void tryBootstrap() + + const onSessionChange = () => void tryBootstrap() + window.addEventListener("crema:session-change", onSessionChange) + return () => { + cancelled = true + window.removeEventListener("crema:session-change", onSessionChange) + } + }, [arcadia]) + + return null +} diff --git a/app/root.tsx b/app/root.tsx index 0458807..33eff63 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -14,6 +14,7 @@ import { ToastProvider, Toaster } from "@crema/notification-ui" import { CommandBusProvider } from "@crema/action-bus" import { ArcadiaProvider } from "@crema/arcadia-client" import { LlmConfigBootstrap } from "~/lib/llm-config-bootstrap" +import { ProfileBootstrap } from "~/lib/profile-bootstrap" // CREMA:PROVIDERS-IMPORTS const ARCADIA_URL = import.meta.env.VITE_ARCADIA_URL ?? "http://localhost:4000" @@ -63,6 +64,7 @@ export default function App() { > + diff --git a/app/routes/profile.tsx b/app/routes/profile.tsx index 85dde38..76f89af 100644 --- a/app/routes/profile.tsx +++ b/app/routes/profile.tsx @@ -115,7 +115,17 @@ export default function ProfileRoute() { timezone: p.timezone ?? "", }) const url = pickAvatarUrl(p) - if (url) setPrefs((d) => ({ ...d, avatarUrl: url })) + if (url) { + // Persist into localStorage so the appbar's useProfile() picks + // it up on next render — without this, the appbar avatar reverts + // to initials on every fresh browser session until the user + // re-uploads. + setPrefs((d) => { + const next = { ...d, avatarUrl: url } + saveProfile(next) + return next + }) + } } } catch (err) { setAccountError(