diff --git a/src/app/insights/page-client.tsx b/src/app/insights/page-client.tsx
index de0233ea3..fb3c162be 100644
--- a/src/app/insights/page-client.tsx
+++ b/src/app/insights/page-client.tsx
@@ -7,6 +7,7 @@ import Link from "next/link";
import { SlidersHorizontal, TrendingUp } from "lucide-react";
import { useAuth } from "@/hooks/use-auth";
+import { accountName } from "@/lib/account-name";
import { queryKeys } from "@/lib/query-keys";
import { apiGet } from "@/lib/api/api-fetch";
// Bare number — its own React- and label-map-free module, so importing it here
@@ -419,10 +420,9 @@ export default function InsightsPageClient() {
// boundary hydrates, and a greeting that personalises during the
// hydration render disagrees with the name-less SSR text — React
// #418. See `useMounted`.
+ const shownName = user ? accountName(user).trim() : "";
const heroGreetingName =
- mounted && user?.username?.trim() && user.username.trim().length > 0
- ? user.username.split(/\s+/)[0]
- : null;
+ mounted && shownName.length > 0 ? shownName.split(/\s+/)[0] : null;
// The advisor read only fires while the briefing is available, so with it
// unavailable these stay null: the hero keeps its fallback subtitle and the
// trends row its built-in captions.
diff --git a/src/components/dashboard/dashboard-header.tsx b/src/components/dashboard/dashboard-header.tsx
index 1a7308bff..c4e70a63b 100644
--- a/src/components/dashboard/dashboard-header.tsx
+++ b/src/components/dashboard/dashboard-header.tsx
@@ -32,6 +32,7 @@ import {
import { PageHeader } from "@/components/ui/page-header";
import { useTranslations } from "@/lib/i18n/context";
import { useAuth } from "@/hooks/use-auth";
+import { accountName } from "@/lib/account-name";
import { useMounted } from "@/hooks/use-mounted";
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { hourInTz } from "@/lib/tz/format";
@@ -143,7 +144,7 @@ export function DashboardHeader({
const greeting = greetingLine({
greeting: timeGreeting,
known: mounted && user != null,
- name: user?.username?.trim() ? user.username : null,
+ name: user ? accountName(user).trim() || null : null,
createdAt: user?.createdAt,
});
const welcomeText = t(greeting.key, greeting.values);
diff --git a/src/components/layout/__tests__/sidebar-nav.test.tsx b/src/components/layout/__tests__/sidebar-nav.test.tsx
index c6d519608..a79eda4d1 100644
--- a/src/components/layout/__tests__/sidebar-nav.test.tsx
+++ b/src/components/layout/__tests__/sidebar-nav.test.tsx
@@ -18,6 +18,7 @@ const mockUserRef = {
value: {
id: "u1",
username: "testuser",
+ displayName: null as string | null,
email: "user@example.com",
role: "USER" as "USER" | "ADMIN",
avatarUrl: null,
@@ -119,6 +120,25 @@ const GATED_HREFS = [
"/achievements",
] as const;
+describe("
{user.username}
+{name}
{user.email && ({user.email}
)} @@ -282,11 +284,9 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {{user.username}
+{name}
{user.email && (
{user.email}
diff --git a/src/components/layout/top-bar.tsx b/src/components/layout/top-bar.tsx
index 23988f100..4d499d1a2 100644
--- a/src/components/layout/top-bar.tsx
+++ b/src/components/layout/top-bar.tsx
@@ -26,6 +26,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Skeleton } from "@/components/ui/skeleton";
import Link from "next/link";
import { useAuth, useLogout } from "@/hooks/use-auth";
+import { accountName } from "@/lib/account-name";
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { useTheme } from "@/components/providers";
import { useTranslations } from "@/lib/i18n/context";
@@ -143,13 +144,13 @@ export function TopBar() {
{t("nav.userMenu")}
- {user.username}
+ {accountName(user)}