fix(white-label): back-to-clients link points at the byra cockpit's home domain (#1973)

* fix(white-label): back-to-clients link points at the byra cockpit's home domain

A byra member working a company homed on another host (e.g. a pre-byra
company on canonical) got a relative /clients on the wrong host instead
of their white-label cockpit. resolveCockpitHref mirrors WL-14's home
rule: relative when the current host is the cockpit's home (brand domain,
or canonical for a brandless byra), else an absolute URL there. Cross-
host links render a plain <a> with a 'Hanteras via' hint; the hop lands
on the brand host's login (per-host sessions, WL-01) and WL-14 then
lands on /clients.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(white-label): show the cross-host cockpit hint as visible text

CodeRabbit: title-only hints never surface on touch devices, so the
expanded-sidebar and mobile external back-links now render the
'Hanteras via {domain}' line as small muted text under the label
(same pattern as the switcher's foreign entries). Also corrects the
comments claiming the no-company branch never renders the back-link:
it can, on its cockpit/settings surfaces, where the relative fallback
matches pre-change behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-27 13:44:23 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent a860c690ed
commit 99c94d467a
5 changed files with 240 additions and 25 deletions
+96 -20
View File
@@ -340,6 +340,7 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
const { identity: agentIdentity } = useAgentSheet()
const tNav = useTranslations('nav')
const tCommon = useTranslations('common')
const tSwitcher = useTranslations('company_switcher')
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
const [isClosing, setIsClosing] = useState(false)
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
@@ -374,11 +375,27 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
}
const cockpitMode =
!!byraTeam && (onSettings ? lastNonSettingsCockpit || ctxByra : onCockpitPath)
// Where "Tillbaka till klienter" points (home-domain rule, WL-01): absolute
// URL when the byrå cockpit is homed on another host (resolveCockpitHref in
// the layout), relative '/clients' otherwise. The layout's pre-brand
// no-company branch leaves cockpitHref unset; there the relative fallback
// reproduces the pre-cockpitHref link exactly on the cockpit/settings
// surfaces that branch can render. Cross-host hops render a plain <a>
// (no client-router prefetch across origins) and carry a "Hanteras via"
// hint, since the other host will ask for a login (per-host sessions).
const cockpitHref = byraTeam?.cockpitHref ?? '/clients'
const cockpitExternal = cockpitHref.startsWith('https://')
const cockpitHint = cockpitExternal
? tSwitcher('managed_via', { domain: new URL(cockpitHref).hostname })
: null
// Byrå-scope surfaces, not company surfaces: they must stay reachable even
// when the active company is unresolved.
const ALWAYS_ENABLED = new Set(['/settings', '/clients', '/byra', '/byra/automations', '/byra/kpi'])
const isItemEnabled = (href: string) => {
const base = href.split('?')[0]
// The back-to-clients link may be absolute (cross-host cockpit); judge
// it by its path so it stays as reachable as the relative form.
const path = href.startsWith('https://') ? new URL(href).pathname : href
const base = path.split('?')[0]
return hasCompany || ALWAYS_ENABLED.has(base) || base.startsWith('/settings')
}
type ExpandableGroup = Exclude<GroupKey, 'top'>
@@ -726,12 +743,22 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
// Collapsed 64px rail: icon-only rows, native title tooltips, count
// bubbles pinned to the icon corner.
const renderRailItem = (
item: { href: string; labelKey: NavLabelKey; icon: typeof LayoutDashboard; comingSoon?: boolean },
item: {
href: string
labelKey: NavLabelKey
icon: typeof LayoutDashboard
comingSoon?: boolean
/** Cross-origin href: render a plain <a>, no client-router prefetch. */
external?: boolean
/** Extra tooltip context (e.g. "Hanteras via {domain}"). */
hint?: string | null
},
) => {
const active = isActive(item.href)
const enabled = isItemEnabled(item.href) && !item.comingSoon
const badge = badgeFor(item.href)
const label = tNav(item.labelKey)
const title = item.hint ? `${label} · ${item.hint}` : label
const inner = (
<span className="relative">
{renderNavIcon(
@@ -751,12 +778,19 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
? cn('transition-colors duration-150', active ? 'bg-secondary' : 'hover:bg-secondary/60')
: 'opacity-40 cursor-not-allowed',
)
if (enabled && item.external) {
return (
<a key={item.href} href={item.href} className={baseClass} title={title} aria-label={title}>
{inner}
</a>
)
}
return enabled ? (
<NavLink key={item.href} href={item.href} className={baseClass} title={label} aria-label={label}>
<NavLink key={item.href} href={item.href} className={baseClass} title={title} aria-label={title}>
{inner}
</NavLink>
) : (
<div key={item.href} className={baseClass} title={label} aria-disabled="true">
<div key={item.href} className={baseClass} title={title} aria-disabled="true">
{inner}
</div>
)
@@ -823,7 +857,13 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
{/* Rail counterpart of the back-to-clients link. */}
{byraTeam &&
!cockpitMode &&
renderRailItem({ href: '/clients', labelKey: 'back_to_clients', icon: ArrowLeft })}
renderRailItem({
href: cockpitHref,
labelKey: 'back_to_clients',
icon: ArrowLeft,
external: cockpitExternal,
hint: cockpitHint,
})}
{railItems.map((item) => renderRailItem(item))}
{visibleExtensionNavItems.map((item) => {
const Icon = resolveIcon(item.icon)
@@ -867,13 +907,32 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
cockpit, above everything, separated by a hairline. */}
{byraTeam && !cockpitMode && (
<div className="mb-2">
<NavLink
href="/clients"
className="group flex items-center px-3 py-[7px] text-[13px] rounded-lg text-muted-foreground hover:text-foreground hover:bg-secondary/60 transition-colors duration-150"
>
<ArrowLeft className="mr-2.5 h-[15px] w-[15px] flex-shrink-0 text-muted-foreground group-hover:text-foreground" />
<span className="flex-1">{tNav('back_to_clients')}</span>
</NavLink>
{cockpitExternal ? (
<a
href={cockpitHref}
title={cockpitHint ?? undefined}
className="group flex items-center px-3 py-[7px] text-[13px] rounded-lg text-muted-foreground hover:text-foreground hover:bg-secondary/60 transition-colors duration-150"
>
<ArrowLeft className="mr-2.5 h-[15px] w-[15px] flex-shrink-0 text-muted-foreground group-hover:text-foreground" />
<span className="flex-1 min-w-0">
<span className="block">{tNav('back_to_clients')}</span>
{/* Visible cross-host hint: title alone never surfaces
on touch, and the hop lands on the other host's
login (per-host sessions), so say where it goes. */}
<span className="block truncate text-[11px] text-muted-foreground">
{cockpitHint}
</span>
</span>
</a>
) : (
<NavLink
href={cockpitHref}
className="group flex items-center px-3 py-[7px] text-[13px] rounded-lg text-muted-foreground hover:text-foreground hover:bg-secondary/60 transition-colors duration-150"
>
<ArrowLeft className="mr-2.5 h-[15px] w-[15px] flex-shrink-0 text-muted-foreground group-hover:text-foreground" />
<span className="flex-1">{tNav('back_to_clients')}</span>
</NavLink>
)}
<div className="mx-3 mt-2 border-t border-border/60" />
</div>
)}
@@ -1099,14 +1158,31 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa
{/* Byrå members inside a company: route back to the cockpit. */}
{byraTeam && !cockpitMode && (
<div className="mb-1.5">
<NavLink
href="/clients"
onClick={closeMobileMenu}
className="flex items-center gap-3 px-3 min-h-[44px] rounded-lg text-foreground active:bg-muted/60 transition-colors"
>
<ArrowLeft className="h-[18px] w-[18px] flex-shrink-0 text-muted-foreground" />
<span className="text-sm flex-1">{tNav('back_to_clients')}</span>
</NavLink>
{cockpitExternal ? (
<a
href={cockpitHref}
onClick={closeMobileMenu}
className="flex items-center gap-3 px-3 min-h-[44px] rounded-lg text-foreground active:bg-muted/60 transition-colors"
>
<ArrowLeft className="h-[18px] w-[18px] flex-shrink-0 text-muted-foreground" />
<span className="flex-1 min-w-0">
<span className="block text-sm">{tNav('back_to_clients')}</span>
{/* Visible cross-host hint: touch has no title tooltip. */}
<span className="block truncate text-[11px] text-muted-foreground">
{cockpitHint}
</span>
</span>
</a>
) : (
<NavLink
href={cockpitHref}
onClick={closeMobileMenu}
className="flex items-center gap-3 px-3 min-h-[44px] rounded-lg text-foreground active:bg-muted/60 transition-colors"
>
<ArrowLeft className="h-[18px] w-[18px] flex-shrink-0 text-muted-foreground" />
<span className="text-sm flex-1">{tNav('back_to_clients')}</span>
</NavLink>
)}
<div className="mx-3 mt-1.5 h-px bg-border/30" />
</div>
)}