Files
HealthLog/src/components/layout/shared-record-unavailable.tsx
T
Marc-André Bombeck dd3d5646de feat(sharing): the switch, the banner, and the nav inside another record
Switching is three acts in a fixed order, and the order is the safety
argument. Ask the server, so a refused switch leaves the browser where it was.
Empty the caches a reload cannot reach, awaited rather than fired, because a
wipe racing the navigation is a wipe that sometimes does not happen. Then
reload the document, which is what discards the in-memory cache, the rendered
React trees, and every request already in flight against the old record.

The banner is the feature, not decoration. Someone who forgets they are
switched will log their own blood pressure into another person's record, and
there is no undo for a reading that was never theirs. So it states whose
record is open, that it is read-only, and carries the way out — in the one
visual register reserved for "something about this screen is not normal", at
the top of the shell above every other strip. A subtle tint is what people
stop seeing on the third switch.

The switcher lives in one component mounted in both user menus. A way in on
the laptop and no way out on the phone is how the two menus have drifted
before.

Nav entries that sharing does not cover drop out, and a page reached by
bookmark says so instead of painting a row of refusals. Both are paint: the
server refuses those routes on its own, from a frozen list this code cannot
reach. Settings, connected services, notifications and the AI surfaces are the
account around a record rather than the record itself.

A grant can end while a browser is inside the record. Revocation clears the
delegate's sessions in the same transaction; expiry does not, and leaves a
session stamped with an account it may no longer act on — correct refusals
under a screen that lies. One cache subscriber notices the stable errorCode
and leaves, rather than asking two hundred call sites to remember.
2026-08-02 14:26:26 +02:00

53 lines
1.9 KiB
TypeScript

"use client";
import { Lock } from "lucide-react";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { useAccountSwitch } from "@/hooks/use-account-switch";
import { useTranslations } from "@/lib/i18n/context";
/**
* v1.36.0 — the page a delegate reached that sharing does not cover.
*
* The nav drops these destinations while a switch is on, so the way here is a
* bookmark, a browser back button, or a link somebody kept open. Without this
* panel the page would mount, fire its reads, collect a row of 403s, and paint
* a screen full of error cards that each say something went wrong — when
* nothing did.
*
* It says the honest thing instead: this surface is not part of what sharing
* covers, and here is the way back to your own record. Settings, credentials,
* integrations, notification channels and the AI surfaces are the account
* around a health record rather than the record itself; a delegate reads the
* record.
*
* Paint, not enforcement. Every route behind these pages refuses on its own,
* from a frozen allowlist this component cannot reach — if this file were
* deleted the delegate would see 403s, not data.
*/
export function SharedRecordUnavailable() {
const { t } = useTranslations();
const switchAccount = useAccountSwitch();
return (
<div data-slot="shared-record-unavailable" className="py-10">
<EmptyState
icon={<Lock className="size-6" />}
title={t("recordSharing.unavailable.title")}
description={t("recordSharing.unavailable.description")}
ctaSize="lg"
action={
<Button
data-slot="shared-record-unavailable-leave"
disabled={switchAccount.isPending}
onClick={() => switchAccount.mutate(null)}
>
{t("recordSharing.unavailable.leave")}
</Button>
}
/>
</div>
);
}