mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
Found by an adversarial pass over the release candidate before it was tagged, and it is the reason the tag waited. `POST /api/medications/intake` is admitted for delegates because a caregiver standing next to the patient has to be able to say the tablet went down. It is an update, though, and the same body can carry two other things. Snoozing writes `snoozedUntil` on the OWNER's medication row, and the reminder cron skips that medication for as long as the stamp is in the future. The field takes any future instant. So somebody with write access could stop another person's medication reminders for years, and three things made it quiet: the owner is deliberately not notified of a delegated snooze, the activity feed renders the whole family as "marked a dose", and the only accurate trace was a line in the admin audit log. The second was flipping a dose the owner had already recorded — taken to skipped and back, which rewrites their compliance history and refunds inventory. The consent screen promises that changing what is already there stays with the owner. Both refuse for a delegate now and stay open to the owner, who is the only person either was ever for. A refused attempt is filed under the owner and gets its own sentence in the activity panel rather than borrowing the one for marking a dose, because folding them together is what made the gap dangerous. Two more from the same pass: The consent notice named allergies among the things a delegate may add. That left the admitted set before this release shipped, and it said so in six languages on the one screen where somebody decides who may write into their health record. It also omitted half of what IS admitted. The copy now matches the frozen list, and the test asserts the withdrawal rather than only the presence of the rest. The banner told a WRITE delegate nothing at all — the mode line rendered only in the read case, guarded by a comment stating that write grants did not exist yet. This is the release that made them exist. It matters that the banner say it, because the rule that a refused control is absent rather than disabled rests on the banner naming the mode globally. And a third instance of the substitution this release already fixed twice: the BMI chart divided the owner's weights by the height of whoever was looking. Inside a shared record it draws nothing now rather than a number belonging to two different people.
112 lines
5.0 KiB
TypeScript
112 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import { Eye, Loader2, LogOut } from "lucide-react";
|
|
|
|
import { useAccountSwitch } from "@/hooks/use-account-switch";
|
|
import { useAuth } from "@/hooks/use-auth";
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
import { accountLabel } from "@/lib/sharing/account-access-view";
|
|
|
|
/**
|
|
* v1.36.0 — the strip that says whose record is open.
|
|
*
|
|
* This is the feature, not decoration around it. A caregiver who forgets they
|
|
* are switched will log their own blood pressure into somebody else's record,
|
|
* and there is no undo for a reading that was never theirs. So the banner
|
|
* states three things in one line — that this is not your record, whose it is,
|
|
* and that you can only read it — and carries the way out beside them.
|
|
*
|
|
* ## Why it looks like this
|
|
*
|
|
* Loud on purpose, within the token system. `bg-warning/15` + `border-warning/40`
|
|
* puts it in the one visual register the app reserves for "something about this
|
|
* screen is not normal", above the offline strip and the demo strip in the
|
|
* shell so it is the first line of chrome under any circumstance. Not
|
|
* `destructive` — nothing is broken and nothing is at risk of being lost;
|
|
* warning is the honest register for a context the user has to hold in mind.
|
|
* Not a tint on the page background either: a subtle wash is exactly what
|
|
* somebody stops seeing after the third switch, and the failure mode here is
|
|
* a person who has stopped noticing.
|
|
*
|
|
* The name is `text-foreground` and the qualifier `text-muted-foreground`, per
|
|
* the two-tier text rule — the person's name is content, the read-only note is
|
|
* meta.
|
|
*
|
|
* The row is the inline-action shape from UI-STANDARDS §11 rather than a
|
|
* centred wrapping stack: sentence `min-w-0 flex-1`, action `shrink-0`, so the
|
|
* way out stays beside the sentence instead of dropping onto its own line once
|
|
* the name is long. `<MaintainershipBanner>` is the sibling that already had an
|
|
* action and already had this shape; the two now read the same. The tap target
|
|
* carries the standard's floor (`min-h-11 sm:min-h-9`) — a person leaving
|
|
* somebody else's record on a phone is the one interaction in the stack, and it
|
|
* was a 32px target.
|
|
*
|
|
* Mounted for every authenticated route inside `<AuthShell>` (the
|
|
* `<DemoBanner>` pattern) so no page can forget it, and rendered from
|
|
* `accountAccess.active`, which the server resolves: the banner appears exactly
|
|
* when the server says the session is inside a record, never on a client guess.
|
|
*
|
|
* `role="status"` rather than `alert`: assistive tech announces the context
|
|
* once when it appears, without the interrupt an alert implies. `aria-live` is
|
|
* left to the role's implicit `polite`.
|
|
*/
|
|
export function SharedRecordBanner() {
|
|
const { user } = useAuth();
|
|
const { t } = useTranslations();
|
|
const switchAccount = useAccountSwitch();
|
|
|
|
const active = user?.accountAccess?.active ?? null;
|
|
if (!active) return null;
|
|
|
|
const name = accountLabel(active);
|
|
|
|
return (
|
|
<div
|
|
role="status"
|
|
data-slot="shared-record-banner"
|
|
data-account-id={active.accountId}
|
|
className="bg-warning/15 border-warning/40 flex items-start gap-3 border-b px-3 py-2 text-xs sm:items-center"
|
|
>
|
|
<Eye
|
|
className="text-foreground mt-0.5 size-3.5 shrink-0 sm:mt-0"
|
|
aria-hidden="true"
|
|
/>
|
|
<p className="min-w-0 flex-1 leading-snug">
|
|
<span className="text-foreground font-medium">
|
|
{t("recordSharing.banner.viewing", { name })}
|
|
</span>{" "}
|
|
{/* Resolved server-side, and it says something at BOTH levels now.
|
|
v1.36.1 is the release that made `canWrite` true, and until this
|
|
line changed the write case rendered nothing at all: a delegate who
|
|
could add to somebody's health record was told only whose record it
|
|
was. That matters more than the read case, not less, and the rest of
|
|
the product leans on it — a control the record refuses is absent
|
|
rather than disabled, on the stated grounds that the banner names
|
|
the mode globally. It has to actually do that. */}
|
|
<span className="text-muted-foreground">
|
|
{active.canWrite
|
|
? t("recordSharing.banner.canAdd")
|
|
: t("recordSharing.banner.readOnly")}
|
|
</span>
|
|
</p>
|
|
<button
|
|
type="button"
|
|
data-slot="shared-record-banner-exit"
|
|
onClick={() => switchAccount.mutate(null)}
|
|
disabled={switchAccount.isPending}
|
|
className="text-foreground hover:bg-warning/25 focus-visible:ring-ring inline-flex min-h-11 shrink-0 items-center gap-1.5 rounded-md px-2 py-1 font-medium underline underline-offset-2 focus-visible:ring-2 focus-visible:outline-none disabled:opacity-50 sm:min-h-9"
|
|
>
|
|
{switchAccount.isPending ? (
|
|
<Loader2
|
|
className="size-3.5 animate-spin motion-reduce:animate-none"
|
|
aria-hidden="true"
|
|
/>
|
|
) : (
|
|
<LogOut className="size-3.5" aria-hidden="true" />
|
|
)}
|
|
{t("recordSharing.banner.leave")}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|