fix(tab-guard): name the company the other tab switched to (#2328)

The cross-tab guard dialog said "another tab switched the active company"
without saying to which one, so the two exits read as "your company" versus
"the new one". Resolve the observed company id against the memberships the
shell already ships to the client (switcher list plus the foreign-host
signpost list; no request at the moment the tab is told to stop) and say
"en annan flik har bytt till Demo AB" and "Ladda om som Demo AB". Unknown
ids keep the unnamed wording.

Founder re-confirmed the blocking two-exit design (WL-09) today after a
forensic pass on a real firing; this is copy only, no behaviour change.


Claude-Session: https://claude.ai/code/session_01CQG9jNyxM7mwMUHWBrFUxY

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-09-05 17:27:08 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5.1
parent 473b1fd2eb
commit b0b995c77c
6 changed files with 75 additions and 6 deletions
+25 -4
View File
@@ -12,6 +12,7 @@ import {
guardStore,
isTabMismatch,
requestHasNextActionHeader,
resolveObservedCompanyName,
shouldBlockMutation,
} from '@/lib/company/tab-guard'
@@ -119,10 +120,16 @@ function uninstallFetchGuard(): void {
}
export default function CompanyTabSync() {
const { company } = useCompany()
const { company, companies, foreignCompanies } = useCompany()
const t = useTranslations('company_tab_guard')
const currentCompanyId = company?.id ?? null
const [mismatch, setMismatch] = useState(false)
// The company the other tab switched to, so the dialog can name it: the
// choice between "switch back" and "reload as the new one" is only obvious
// when both sides are named. Read from the guard store at the moment the
// dialog is raised (both the observe path and the blocked-write path set
// observedCompanyId first).
const [observedCompanyId, setObservedCompanyId] = useState<string | null>(null)
const [resolving, setResolving] = useState(false)
useEffect(() => {
@@ -134,7 +141,10 @@ export default function CompanyTabSync() {
// A self-initiated switch is hard-navigating this tab away: blocked
// stray writes still get their 409, but the "switched in another tab"
// dialog would just flash over the tab's own page load.
if (!guardStore.selfSwitchTargetId) setMismatch(true)
if (!guardStore.selfSwitchTargetId) {
setObservedCompanyId(guardStore.observedCompanyId)
setMismatch(true)
}
}
installFetchGuard()
@@ -143,6 +153,7 @@ export default function CompanyTabSync() {
guardStore.observedCompanyId = observedId
if (observedId === guardStore.selfSwitchTargetId) return
if (isTabMismatch(currentCompanyId, observedId)) {
setObservedCompanyId(observedId)
setMismatch(true)
}
}
@@ -212,6 +223,12 @@ export default function CompanyTabSync() {
if (!mismatch || !currentCompanyId) return null
const newCompanyName = resolveObservedCompanyName(
observedCompanyId,
companies,
foreignCompanies ?? [],
)
const handleSwitchBack = async () => {
setResolving(true)
// Re-activate THIS tab's company and reload the page we are on (same
@@ -249,7 +266,9 @@ export default function CompanyTabSync() {
{t('title')}
</h2>
<p className="mt-2 text-sm text-muted-foreground">
{t('body', { company: company?.name ?? '' })}
{newCompanyName
? t('body_named', { company: company?.name ?? '', newCompany: newCompanyName })
: t('body', { company: company?.name ?? '' })}
</p>
<div className="mt-6 flex flex-col gap-2 sm:flex-row sm:justify-end">
<Button
@@ -257,7 +276,9 @@ export default function CompanyTabSync() {
disabled={resolving}
onClick={handleReloadAsNew}
>
{t('reload_as_new')}
{newCompanyName
? t('reload_as_named', { newCompany: newCompanyName })
: t('reload_as_new')}
</Button>
<Button disabled={resolving} onClick={() => void handleSwitchBack()}>
{t('switch_back', { company: company?.name ?? '' })}