profile: bootstrap avatar URL on app boot

The appbar's <Avatar> 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) <noreply@anthropic.com>
This commit is contained in:
jules
2026-05-05 09:56:40 +10:00
parent ffe3fc0473
commit 2ab183596c
3 changed files with 60 additions and 1 deletions

View File

@@ -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 <Avatar> 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
}

View File

@@ -14,6 +14,7 @@ import { ToastProvider, Toaster } from "@crema/notification-ui"
import { CommandBusProvider } from "@crema/action-bus" import { CommandBusProvider } from "@crema/action-bus"
import { ArcadiaProvider } from "@crema/arcadia-client" import { ArcadiaProvider } from "@crema/arcadia-client"
import { LlmConfigBootstrap } from "~/lib/llm-config-bootstrap" import { LlmConfigBootstrap } from "~/lib/llm-config-bootstrap"
import { ProfileBootstrap } from "~/lib/profile-bootstrap"
// CREMA:PROVIDERS-IMPORTS // CREMA:PROVIDERS-IMPORTS
const ARCADIA_URL = import.meta.env.VITE_ARCADIA_URL ?? "http://localhost:4000" const ARCADIA_URL = import.meta.env.VITE_ARCADIA_URL ?? "http://localhost:4000"
@@ -63,6 +64,7 @@ export default function App() {
> >
<CommandBusProvider> <CommandBusProvider>
<LlmConfigBootstrap /> <LlmConfigBootstrap />
<ProfileBootstrap />
<Outlet /> <Outlet />
<Toaster /> <Toaster />
</CommandBusProvider> </CommandBusProvider>

View File

@@ -115,7 +115,17 @@ export default function ProfileRoute() {
timezone: p.timezone ?? "", timezone: p.timezone ?? "",
}) })
const url = pickAvatarUrl(p) 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) { } catch (err) {
setAccountError( setAccountError(