From 57fa5ebd88bd055dbeb4518ee019c143a7c54d70 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 05:43:36 +0000 Subject: [PATCH 1/3] Show example content on every Work mode page when signed out Signed out, 18 Work mode pages showed only a sign-in card. They now show clearly marked example content while example data is on, so the whole of Work mode can be seen working before signing in: - CPD: AMA CPD Home, Evidence, Export, Customise and Learning; the old CPD Job applications links follow on to Admin. - Admin: Job applications and CV, the Credential pack (example records and made-up all-zero numbers), and Contract end (an example contract, in the signed-out example set only, never in Load example content). - My Day: Alerts settings, look-only and signed out only. - On Call Who's on from the roster, Roster Manage, and the Open shifts board, Post and Post a shift, through Roster's existing sample gate. Nothing new can save: every example screen still refuses writes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CouthuMe2Xb6tBZ2Rk718N --- ...-signed-out-examples-on-every-work-page.md | 32 +++++++ .../admin/applications/cv/page.tsx | 14 +++- .../(search-app)/admin/applications/page.tsx | 13 ++- src/app/(search-app)/admin/contract/page.tsx | 6 +- .../on-call/whos-on/roster/page.tsx | 8 +- .../(search-app)/open-shifts/board/page.tsx | 7 +- .../open-shifts/post/new/page.tsx | 7 +- .../(search-app)/open-shifts/post/page.tsx | 7 +- src/app/(search-app)/roster/manage/page.tsx | 7 +- .../new-job/admin-credential-pack-page.tsx | 21 ++++- src/components/alerts/alerts-page.tsx | 26 ++++++ src/components/cme/cme-signed-out-sample.tsx | 84 ++++++++++++++++++- .../mode-kit/signed-out-example-switch.tsx | 23 +++++ src/lib/example-data/registry.ts | 5 +- src/lib/on-call/demo-entries.ts | 29 +++++++ tests/admin-credential-pack-page.dom.test.tsx | 16 ++++ tests/cme-signed-out-sample.dom.test.tsx | 21 ++++- ...ll-entry-store-example-switch.dom.test.tsx | 11 ++- tests/on-call-signed-out-sample.dom.test.tsx | 7 +- tests/signed-out-example-switch.dom.test.tsx | 41 +++++++++ 20 files changed, 361 insertions(+), 24 deletions(-) create mode 100644 docs/decisions/2026-10-27-signed-out-examples-on-every-work-page.md create mode 100644 src/components/mode-kit/signed-out-example-switch.tsx create mode 100644 tests/signed-out-example-switch.dom.test.tsx diff --git a/docs/decisions/2026-10-27-signed-out-examples-on-every-work-page.md b/docs/decisions/2026-10-27-signed-out-examples-on-every-work-page.md new file mode 100644 index 0000000000..c532af73b7 --- /dev/null +++ b/docs/decisions/2026-10-27-signed-out-examples-on-every-work-page.md @@ -0,0 +1,32 @@ +# Decision: example content on every Work mode page when signed out + +- **Status:** decided 2026-10-27 +- **Source:** the owner, in chat + +On 27 Oct 2026 the owner asked: "Please ensure there is sample content on all modes for me in +logged out view", "And all pages in work mode", "To demonstrate what it looks like". + +Every Work mode page was opened signed out. Most already showed example records. These pages +showed only a sign-in card, and now show clearly marked example content while example data is +on (the default for a signed-out visitor; turning example data off still gives the plain sign-in +state): + +- CPD: AMA CPD Home, Evidence, Export, Customise and Learning. The old CPD Job applications links + follow on to Admin. +- Admin: Job applications and its CV, the Credential pack (built from the example records with + made-up all-zero numbers, never the numbers saved on the device), and Contract end (an example + contract ending five months out). The example contract is only in the signed-out example set, + never in "Load example content", so a real account never gets an end date it did not enter. +- My Day: Alerts settings, shown as a look-only example while signed out. Signed in, they are + always the reader's own. +- On Call: Who's on from your team roster. +- Roster: Manage team. +- Open shifts: the week board, Post, and Post a shift. + +Left as they were, because they are forms or flows rather than records: Open shifts Alerts (the +same switches as Roster settings, which have nothing to show without phone alerts set up), Log a +shift, My Day Set up and Favourites, the On Call device tools, and Teaching check-in. + +Nothing here can save: example screens refuse writes, as every example screen already did. + +If this summary and the source ever differ, the source wins. diff --git a/src/app/(search-app)/admin/applications/cv/page.tsx b/src/app/(search-app)/admin/applications/cv/page.tsx index 094db88e03..62ea49255e 100644 --- a/src/app/(search-app)/admin/applications/cv/page.tsx +++ b/src/app/(search-app)/admin/applications/cv/page.tsx @@ -2,6 +2,8 @@ import type { Metadata } from "next"; import { ApplicationsCvPage } from "@/components/cme/applications/applications-cv-page"; import { CmeStateNotice } from "@/components/cme/cme-state-notice"; +import { SignedOutExampleSwitch } from "@/components/mode-kit/signed-out-example-switch"; +import { DEMO_CME_ENTRIES, DEMO_CME_INSTANT } from "@/lib/cme/demo-year"; import { loadCmePageData } from "@/lib/cme/load-cme-page-data"; export const metadata: Metadata = { @@ -14,9 +16,15 @@ export default async function CmeApplicationsCvRoute() { const data = await loadCmePageData(undefined, { allYears: true }); if (data.state === "signed-out") { return ( -
- -
+ } + notice={ +
+ +
+ } + /> ); } // A year with no CPD set-up still has other years' activities, read through allYears. diff --git a/src/app/(search-app)/admin/applications/page.tsx b/src/app/(search-app)/admin/applications/page.tsx index 0b613b5026..dff16f7957 100644 --- a/src/app/(search-app)/admin/applications/page.tsx +++ b/src/app/(search-app)/admin/applications/page.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { ApplicationsPage } from "@/components/cme/applications/applications-page"; import { CmeStateNotice } from "@/components/cme/cme-state-notice"; +import { SignedOutExampleSwitch } from "@/components/mode-kit/signed-out-example-switch"; import { cpdYearOf } from "@/lib/cme/cpd-year"; import { DEMO_CME_INSTANT } from "@/lib/cme/demo-year"; import { isDemoMode } from "@/lib/env"; @@ -32,9 +33,15 @@ export default async function CmeApplicationsRoute() { const now = demoMode ? DEMO_CME_INSTANT : new Date(); if (!demoMode && (await signedOut())) { return ( -
- -
+ } + notice={ +
+ +
+ } + /> ); } return ; diff --git a/src/app/(search-app)/admin/contract/page.tsx b/src/app/(search-app)/admin/contract/page.tsx index 30ce10efca..dbcf4a6a9d 100644 --- a/src/app/(search-app)/admin/contract/page.tsx +++ b/src/app/(search-app)/admin/contract/page.tsx @@ -3,6 +3,7 @@ import { Suspense } from "react"; import { ContractEndPage } from "@/components/admin/contract/contract-end-page"; import { ModeHomeRouteLoading } from "@/components/mode-home-page-skeleton"; +import { RosterSampleGate } from "@/components/roster/roster-sample-gate"; export const metadata: Metadata = { title: "Contract end | Admin | PsychSift", @@ -15,7 +16,10 @@ export default function AdminContractRoute() { // Suspense boundary in the App Router. return ( }> - + {/* The leave-before-the-end list reads Roster, so signed out it reads Roster's example leave. */} + + + ); } diff --git a/src/app/(search-app)/on-call/whos-on/roster/page.tsx b/src/app/(search-app)/on-call/whos-on/roster/page.tsx index 1e7745cb48..b6277c651f 100644 --- a/src/app/(search-app)/on-call/whos-on/roster/page.tsx +++ b/src/app/(search-app)/on-call/whos-on/roster/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { OnCallRosterWhosOnPage } from "@/components/on-call/roster-whos-on/on-call-roster-whos-on-page"; +import { RosterSampleGate } from "@/components/roster/roster-sample-gate"; export const metadata: Metadata = { title: "From your team roster | On Call | PsychSift", @@ -9,5 +10,10 @@ export const metadata: Metadata = { }; export default function OnCallRosterWhosOnRoute() { - return ; + // Who is on reads Roster, so signed out it shows Roster's example team. + return ( + + + + ); } diff --git a/src/app/(search-app)/open-shifts/board/page.tsx b/src/app/(search-app)/open-shifts/board/page.tsx index bfc893eaad..677aacede2 100644 --- a/src/app/(search-app)/open-shifts/board/page.tsx +++ b/src/app/(search-app)/open-shifts/board/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { OpenShiftsBoardPage } from "@/components/open-shifts/open-shifts-board-page"; +import { RosterSampleGate } from "@/components/roster/roster-sample-gate"; export const metadata: Metadata = { title: "Board | Open shifts | PsychSift", @@ -8,5 +9,9 @@ export const metadata: Metadata = { }; export default function OpenShiftsBoardRoute() { - return ; + return ( + + + + ); } diff --git a/src/app/(search-app)/open-shifts/post/new/page.tsx b/src/app/(search-app)/open-shifts/post/new/page.tsx index d6e0ee583b..f514c7196f 100644 --- a/src/app/(search-app)/open-shifts/post/new/page.tsx +++ b/src/app/(search-app)/open-shifts/post/new/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { OpenShiftsPostPage } from "@/components/open-shifts/open-shifts-post-page"; +import { RosterSampleGate } from "@/components/roster/roster-sample-gate"; export const metadata: Metadata = { title: "Post a shift | Open shifts | PsychSift", @@ -8,5 +9,9 @@ export const metadata: Metadata = { }; export default function OpenShiftsPostRoute() { - return ; + return ( + + + + ); } diff --git a/src/app/(search-app)/open-shifts/post/page.tsx b/src/app/(search-app)/open-shifts/post/page.tsx index 5d3651660b..4d2c53a642 100644 --- a/src/app/(search-app)/open-shifts/post/page.tsx +++ b/src/app/(search-app)/open-shifts/post/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { OpenShiftsPostedPage } from "@/components/open-shifts/open-shifts-posted-page"; +import { RosterSampleGate } from "@/components/roster/roster-sample-gate"; export const metadata: Metadata = { title: "Post | Open shifts | PsychSift", @@ -8,5 +9,9 @@ export const metadata: Metadata = { }; export default function OpenShiftsPostedRoute() { - return ; + return ( + + + + ); } diff --git a/src/app/(search-app)/roster/manage/page.tsx b/src/app/(search-app)/roster/manage/page.tsx index 6f720cd443..a60b672a66 100644 --- a/src/app/(search-app)/roster/manage/page.tsx +++ b/src/app/(search-app)/roster/manage/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { RosterManagePage } from "@/components/roster/manage/roster-manage-page"; +import { RosterSampleGate } from "@/components/roster/roster-sample-gate"; export const metadata: Metadata = { title: "Manage team | Roster | PsychSift", @@ -8,5 +9,9 @@ export const metadata: Metadata = { }; export default function Page() { - return ; + return ( + + + + ); } diff --git a/src/components/admin/new-job/admin-credential-pack-page.tsx b/src/components/admin/new-job/admin-credential-pack-page.tsx index a3fa482577..6a988680ea 100644 --- a/src/components/admin/new-job/admin-credential-pack-page.tsx +++ b/src/components/admin/new-job/admin-credential-pack-page.tsx @@ -43,6 +43,14 @@ import { guardExampleAction } from "@/lib/example-data/guards"; * The wallet is device-only, so the pack is offered only when signed in and * not in the example corpus, the same rule Admin Today uses for the wallet. */ +/** Made-up numbers for the signed-out example, shaped like the real ones but all zeros. */ +const EXAMPLE_PACK_NUMBERS: CredentialPackNumbers = { + ahpraNumber: "MED0000000000", + prescriberNumber: "0000000", + wwccNumber: "0000000", + providerNumbers: [{ id: "example-provider", site: "Example Hospital", number: "000000AB" }], +}; + export function AdminCredentialPackPage({ now: nowProp }: { now?: Date } = {}) { const state = useOnCallEntries(); const { isAuthenticated } = useAccountData(); @@ -55,7 +63,11 @@ export function AdminCredentialPackPage({ now: nowProp }: { now?: Date } = {}) { const [numbers, setNumbers] = useState(() => loadDoctorCredentials()); useEffect(() => subscribeAccountTransition(() => setNumbers({})), []); - const sections = useMemo(() => buildCredentialPack({ numbers, ownEntries: own }), [numbers, own]); + // Signed out with example data showing, the pack is built from the example records and made-up + // numbers, so the page can be seen working. Copy, share and save stay off for examples. + const signedOutExample = !isAuthenticated && state.sample; + const packNumbers = signedOutExample ? EXAMPLE_PACK_NUMBERS : numbers; + const sections = useMemo(() => buildCredentialPack({ numbers: packNumbers, ownEntries: own }), [packNumbers, own]); const [excluded, setExcluded] = useState>(() => new Set()); const included = useMemo(() => includedCredentialPack(sections, excluded), [sections, excluded]); const text = useMemo(() => credentialPackText(included, now), [included, now]); @@ -92,7 +104,7 @@ export function AdminCredentialPackPage({ now: nowProp }: { now?: Date } = {}) { ); } - const available = isAuthenticated && !state.demoMode; + const available = (isAuthenticated && !state.demoMode) || signedOutExample; const ready = loadState === "ready" && available; const nothingIncluded = included.length === 0; @@ -159,6 +171,11 @@ export function AdminCredentialPackPage({ now: nowProp }: { now?: Date } = {}) { ) : ready ? ( <> + {signedOutExample ? ( + + These are example records with made-up numbers. Sign in to make a pack from your own. + + ) : null}
Choose what goes in
    diff --git a/src/components/alerts/alerts-page.tsx b/src/components/alerts/alerts-page.tsx index 5e089406ee..51fdec08ab 100644 --- a/src/components/alerts/alerts-page.tsx +++ b/src/components/alerts/alerts-page.tsx @@ -17,8 +17,11 @@ import { usePhoneAlerts } from "@/components/alerts/use-phone-alerts"; import { useRemindMe } from "@/components/alerts/use-remind-me"; import { useAppPreferences } from "@/components/clinical-dashboard/use-app-preferences"; import { ModeGroupedList, ModeRow } from "@/components/mode-kit/grouped-list"; +import { SignedOutSampleNotice } from "@/components/mode-kit/signed-out-sample"; +import { useSignedOut } from "@/components/mode-kit/use-signed-out-sample"; import { WorkTag } from "@/components/mode-kit/work"; import { MyDayFrame } from "@/components/my-day/my-day-frame"; +import { RosterSampleGate } from "@/components/roster/roster-sample-gate"; import { useRosterSettings } from "@/components/roster/use-roster-settings"; import { useWorkModeRouteVisible } from "@/components/work-mode-launch/work-mode-launch-provider"; import { ALERT_AREA_IDS, ALERT_AREAS, areaSummary, type AlertAreaId } from "@/lib/alerts/areas"; @@ -47,6 +50,9 @@ export function AlertsPage({ */ inFrame?: boolean; } = {}) { + // Signed out, the settings show as an example to look at (the frame adds it only while My Day shows + // example data). Signed in, they are always the reader's own, never an example. + const signedOut = useSignedOut(); return ( "What reaches your phone, and when"} + signedOutSample={signedOut ? { render: (at) => } : undefined} signedOut={{ title: "Sign in to get alerts", body: "Alerts come from your own roster and records, so they need your account. Nothing is sent to this device while you're signed out.", @@ -64,6 +71,25 @@ export function AlertsPage({ ); } +/** + * The settings as a signed-in doctor sees them, to look at only: `inert` keeps every switch and sheet + * shut, so nothing is changed on this device or sent. Roster's switches read Roster's example data. + */ +function AlertsSignedOutSample({ now, inFrame }: { now: Date; inFrame: boolean }) { + return ( + <> + + This is how the settings look. Signed in, you choose what reaches your phone, and when. + + +
    + +
    +
    + + ); +} + /** Each area's icon, as the notification centre draws it. */ const AREA_ICONS: Readonly> = { roster: CalendarDays, diff --git a/src/components/cme/cme-signed-out-sample.tsx b/src/components/cme/cme-signed-out-sample.tsx index 4a7cbe9b46..856595aaca 100644 --- a/src/components/cme/cme-signed-out-sample.tsx +++ b/src/components/cme/cme-signed-out-sample.tsx @@ -2,12 +2,14 @@ import Link from "next/link"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { Suspense } from "react"; +import { Suspense, useEffect } from "react"; import { CmeAnnualSummary } from "@/components/cme/cme-annual-summary"; import { CmeCalendarPage } from "@/components/cme/cme-calendar-page"; +import { CmeCustomisePage } from "@/components/cme/cme-customise-page"; import { CmeDashboard } from "@/components/cme/cme-dashboard"; import { CmeEntryRouteClient } from "@/components/cme/cme-entry-route-client"; +import { CmeLearningPage } from "@/components/cme/cme-learning-page"; import { CmeLogPage, type CmeLogAttention } from "@/components/cme/cme-log-page"; import { CmeNewEntryRoute } from "@/components/cme/cme-new-entry-route"; import { CmePageTabs } from "@/components/cme/cme-page-tabs"; @@ -19,6 +21,9 @@ import { CmeSampleContext } from "@/components/cme/cme-sample-context"; import { CmeSetupRoute } from "@/components/cme/cme-setup-route"; import { CmeTrainingPage } from "@/components/cme/cme-training-page"; import { CmeYearCheckPage } from "@/components/cme/cme-year-check-page"; +import { CpdHomeSendPage } from "@/components/cme/cpd-home/cpd-home-send-page"; +import { CpdEvidencePage } from "@/components/work-screens/cpd/cpd-evidence-page"; +import { CpdExportPage } from "@/components/work-screens/cpd/cpd-export-page"; import { perthCalendarDate } from "@/lib/cme/cpd-year"; import { DEMO_CME_ENTRIES, @@ -27,6 +32,7 @@ import { DEMO_CME_ROUTINES, DEMO_CME_YEAR, } from "@/lib/cme/demo-year"; +import { loadLearningDirectory } from "@/lib/cme/learning-directory"; import { parseCmeLearningPrefill } from "@/lib/cme/learning-source"; import { trainingExampleView } from "@/lib/cme/training-assessments"; import { @@ -36,6 +42,7 @@ import { sampleTrainingAssessments, } from "@/lib/cme/training-assessments-sample"; import { cmeCategories, type CmeCategory } from "@/lib/cme/types"; +import { parseEvidenceCategory, parseEvidenceOrder, parseEvidenceStatus } from "@/lib/work-screens/cpd/evidence"; /** * The signed-out CPD sample: the real screens filled with the invented demo year @@ -52,6 +59,20 @@ const ENTRIES = DEMO_CME_ENTRIES; const ROUTINES = DEMO_CME_ROUTINES; const GOALS = DEMO_CME_PLAN_GOALS; const SAMPLE_YEARS = [DEMO_CME_YEAR.year] as const; +/** The same two years the demo loader offers (`load-cme-page-data`), so the year chips match. */ +const SAMPLE_AVAILABLE_YEARS = [DEMO_CME_YEAR.year, DEMO_CME_YEAR.year - 1]; + +/** Pages that moved to Admin: the server redirect does not run here, so follow it in the browser. */ +const MOVED_TO_ADMIN: Readonly> = { + "/cme/applications": "/admin/applications", + "/cme/applications/cv": "/admin/applications/cv", +}; + +function MovedPage({ href }: { readonly href: string }) { + const router = useRouter(); + useEffect(() => router.replace(href), [href, router]); + return
    ; +} function SampleBody({ pathname, query }: { readonly pathname: string; readonly query: URLSearchParams }) { const router = useRouter(); @@ -160,6 +181,67 @@ function SampleBody({ pathname, query }: { readonly pathname: string; readonly q ); } + if (pathname === "/cme/cpd-home") { + return ( + + ); + } + if (pathname === "/cme/evidence") { + const category = parseEvidenceCategory(query.get("category")); + const status = parseEvidenceStatus(query.get("show")); + const order = parseEvidenceOrder(query.get("order")); + return ( + + ); + } + if (pathname === "/cme/export") { + return ( + + ); + } + if (pathname === "/cme/customise") { + // Only the dashboard layout on this device; no CPD record is read or written. + return ; + } + if (pathname === "/cme/learning") { + // The public WA course list, the same for everyone; the home label is the sample year's. + const directory = loadLearningDirectory(); + return ( + + ); + } + const moved = MOVED_TO_ADMIN[pathname]; + if (moved) return ; return (

    This page is not part of the example data. It works on your own CPD record, so it needs you to sign in.

    diff --git a/src/components/mode-kit/signed-out-example-switch.tsx b/src/components/mode-kit/signed-out-example-switch.tsx new file mode 100644 index 0000000000..18746f82e9 --- /dev/null +++ b/src/components/mode-kit/signed-out-example-switch.tsx @@ -0,0 +1,23 @@ +"use client"; + +import type { ReactNode } from "react"; + +import { useSignedOutSample } from "@/components/mode-kit/use-signed-out-sample"; +import type { WorkAreaId } from "@/lib/work-frame/areas"; + +/** + * For a server page that already knows the visitor is signed out: the page's + * example while the example data switch shows examples in `area`, else the + * page's own sign-in notice. Only one of the two is ever mounted. + */ +export function SignedOutExampleSwitch({ + area, + example, + notice, +}: { + readonly area: WorkAreaId; + readonly example: ReactNode; + readonly notice: ReactNode; +}) { + return useSignedOutSample(area) ? example : notice; +} diff --git a/src/lib/example-data/registry.ts b/src/lib/example-data/registry.ts index cb7ef0814f..4e606d249f 100644 --- a/src/lib/example-data/registry.ts +++ b/src/lib/example-data/registry.ts @@ -75,7 +75,10 @@ const LOADERS: { [K in ExampleDatasetKey]: Loader } = { "roster.rotations": async (now) => (await import("@/lib/example-data/datasets/roster-rotations")).exampleRotationRounds(now), "openShifts.listings": async (now) => (await import("@/lib/open-shifts/sample")).sampleListings(now), - "onCall.entries": async () => (await import("@/lib/on-call/demo-entries")).DEMO_ON_CALL_ENTRIES, + "onCall.entries": async () => { + const { DEMO_ON_CALL_ENTRIES, EXAMPLE_CONTRACT_END_ENTRY } = await import("@/lib/on-call/demo-entries"); + return [...DEMO_ON_CALL_ENTRIES, EXAMPLE_CONTRACT_END_ENTRY]; + }, "teaching.sessions": async (now, zone) => { const today = zonedDateOf(now, zone); return (await import("@/lib/teaching/demo-programme")).demoTeachingSessions( diff --git a/src/lib/on-call/demo-entries.ts b/src/lib/on-call/demo-entries.ts index eb66b5cb17..77b64a909c 100644 --- a/src/lib/on-call/demo-entries.ts +++ b/src/lib/on-call/demo-entries.ts @@ -2257,3 +2257,32 @@ export const DEMO_ON_CALL_ENTRIES: readonly OnCallEntry[] = [ lastVerifiedAt: NEVER_VERIFIED, }), ]; + +/** + * The example contract, five months out, so Contract end shows its dates, reminders + * and questions to a signed-out visitor. It is only in the example data set + * (`onCall.entries`), never in the corpus above: "Load example content" must + * not give a real account a contract end date it did not enter. + */ +export const EXAMPLE_CONTRACT_END_ENTRY: OnCallEntry = entry({ + id: id(141), + section: "logistics", + slug: "contract-end-example", + title: "Contract end", + subtitle: null, + body: null, + details: { + category: "Personal", + kind: "compliance", + expiresOn: demoDateKey(150), + leadTimeDays: 91, + issuingBody: "Example Health Service", + provenance: "typed", + }, + linkedDocumentIds: [], + tags: [], + isPersonal: true, + includeOnCard: false, + sortOrder: 19, + lastVerifiedAt: VERIFIED_RECENTLY, +}); diff --git a/tests/admin-credential-pack-page.dom.test.tsx b/tests/admin-credential-pack-page.dom.test.tsx index 74060a3b25..90db1253da 100644 --- a/tests/admin-credential-pack-page.dom.test.tsx +++ b/tests/admin-credential-pack-page.dom.test.tsx @@ -27,6 +27,7 @@ const entryState = vi.hoisted(() => ({ loadError: null as "offline" | "failed" | null, signedOut: false, demoMode: false, + sample: false, cachedAt: null as string | null, retry: vi.fn(), })); @@ -52,6 +53,7 @@ beforeEach(() => { loadError: null, signedOut: false, demoMode: false, + sample: false, cachedAt: null, }); }); @@ -70,6 +72,20 @@ describe("AdminCredentialPackPage", () => { expect(screen.queryByTestId("admin-credential-pack-preview")).toBeNull(); }); + it("shows a pack built from the example records and made-up numbers when signed out with examples showing", () => { + accountState.isAuthenticated = false; + seedWallet({ ahpraNumber: "MED1111111111" }); + Object.assign(entryState, { demoMode: true, sample: true, entries: [renewal] }); + render(); + expect(screen.getByTestId("admin-credential-pack-example").textContent).toContain("made-up numbers"); + const preview = screen.getByTestId("admin-credential-pack-preview").textContent ?? ""; + expect(preview).toContain("MED0000000000"); + expect(preview).toContain("Medical registration renewal"); + // Whatever this device holds is never shown in the example. + expect(preview).not.toContain("MED1111111111"); + expect(screen.queryByTestId("admin-credential-pack-signed-out")).toBeNull(); + }); + it("shows the same message in demo mode", () => { Object.assign(entryState, { demoMode: true }); render(); diff --git a/tests/cme-signed-out-sample.dom.test.tsx b/tests/cme-signed-out-sample.dom.test.tsx index 8c8000b1c5..afdfa737fc 100644 --- a/tests/cme-signed-out-sample.dom.test.tsx +++ b/tests/cme-signed-out-sample.dom.test.tsx @@ -121,14 +121,31 @@ describe("CPD signed-out sample", () => { }); it("says plainly that a page outside the sample needs sign-in", async () => { - nav.pathname = "/cme/customise"; + nav.pathname = "/cme/not-a-sample-page"; renderBoundary(); await sampleShown(); expect(screen.getByTestId("cme-sample-unavailable").textContent).toContain("not part of the example data"); }); + it("follows Job applications to Admin, where it moved", async () => { + nav.pathname = "/cme/applications/cv"; + renderBoundary(); + await waitFor(() => expect(router.replace).toHaveBeenCalledWith("/admin/applications/cv")); + }); + it("makes no network call and no browser-storage write on any sample page", async () => { - for (const pathname of ["/cme", "/cme/log", "/cme/routines", "/cme/summary", "/cme/new", "/cme/training"]) { + for (const pathname of [ + "/cme", + "/cme/log", + "/cme/routines", + "/cme/summary", + "/cme/new", + "/cme/training", + "/cme/cpd-home", + "/cme/evidence", + "/cme/export", + "/cme/learning", + ]) { nav.pathname = pathname; const view = renderBoundary(); await sampleShown(); diff --git a/tests/on-call-entry-store-example-switch.dom.test.tsx b/tests/on-call-entry-store-example-switch.dom.test.tsx index c05fecbba8..2b806241df 100644 --- a/tests/on-call-entry-store-example-switch.dom.test.tsx +++ b/tests/on-call-entry-store-example-switch.dom.test.tsx @@ -10,7 +10,7 @@ import { cleanup, renderHook, waitFor } from "@testing-library/react"; import type { ReactNode } from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { DEMO_ON_CALL_ENTRIES } from "@/lib/on-call/demo-entries"; +import { DEMO_ON_CALL_ENTRIES, EXAMPLE_CONTRACT_END_ENTRY } from "@/lib/on-call/demo-entries"; import { OnCallExampleDataScope, cacheOnCallEntries, @@ -21,6 +21,9 @@ import { } from "@/lib/on-call/entry-store"; import { areaDataState, resetExampleDataForTests, setExampleDataOn } from "@/lib/example-data/store"; +/** The example set is the corpus plus the example contract, which "Load example content" never writes. */ +const EXAMPLE_SET_SIZE = [...DEMO_ON_CALL_ENTRIES, EXAMPLE_CONTRACT_END_ENTRY].length; + const auth = vi.hoisted(() => ({ status: "authenticated" as string, authEpoch: 1, @@ -64,7 +67,7 @@ describe("useOnCallEntries and the example data switch", () => { await waitFor(() => expect(result.current.sample).toBe(true)); await waitFor(() => expect(result.current.loading).toBe(false)); expect(result.current.demoMode).toBe(true); - expect(result.current.entries).toHaveLength(DEMO_ON_CALL_ENTRIES.length); + expect(result.current.entries).toHaveLength(EXAMPLE_SET_SIZE); expect(result.current.entries.every(isOnCallExampleEntry)).toBe(true); expect(result.current.cachedAt).toBeNull(); @@ -111,7 +114,7 @@ describe("useOnCallEntries and the example data switch", () => { await waitFor(() => expect(result.current.loading).toBe(false)); expect(areaDataState("call")).toBe("empty"); expect(result.current.demoMode).toBe(true); - expect(result.current.entries).toHaveLength(DEMO_ON_CALL_ENTRIES.length); + expect(result.current.entries).toHaveLength(EXAMPLE_SET_SIZE); }); it("honours an explicit off for a signed-out visitor: no sample, the normal signed-out state", async () => { @@ -134,7 +137,7 @@ describe("useOnCallEntries and the example data switch", () => { const { result } = renderHook(() => useOnCallEntries(), { wrapper: scope }); await waitFor(() => expect(result.current.sample).toBe(true)); await waitFor(() => expect(result.current.loading).toBe(false)); - expect(result.current.entries).toHaveLength(DEMO_ON_CALL_ENTRIES.length); + expect(result.current.entries).toHaveLength(EXAMPLE_SET_SIZE); }); it("changes nothing outside the frame's scope", async () => { diff --git a/tests/on-call-signed-out-sample.dom.test.tsx b/tests/on-call-signed-out-sample.dom.test.tsx index 1cad0fff1b..f87bba04af 100644 --- a/tests/on-call-signed-out-sample.dom.test.tsx +++ b/tests/on-call-signed-out-sample.dom.test.tsx @@ -7,11 +7,14 @@ import { cleanup, renderHook, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { DEMO_ON_CALL_ENTRIES } from "@/lib/on-call/demo-entries"; +import { DEMO_ON_CALL_ENTRIES, EXAMPLE_CONTRACT_END_ENTRY } from "@/lib/on-call/demo-entries"; import { clearOnCallEntryCache, onCallEntryCacheStorageKey, useOnCallEntries } from "@/lib/on-call/entry-store"; import { sampleServiceDetail } from "@/lib/on-call/service-demo"; import { isOnCallPlaceholderNumber, onCallTelHref, resolveHandbookPhone } from "@/lib/on-call/number-resolver"; +/** The example set is the corpus plus the example contract, which "Load example content" never writes. */ +const EXAMPLE_SET_SIZE = [...DEMO_ON_CALL_ENTRIES, EXAMPLE_CONTRACT_END_ENTRY].length; + const auth = vi.hoisted(() => ({ status: "signed_out", authEpoch: 1 })); vi.mock("@/lib/supabase/client", () => ({ useAuthSession: () => auth })); vi.mock("@/components/clinical-dashboard/account-setup-dialog", () => ({ @@ -40,7 +43,7 @@ describe("On Call signed-out sample", () => { await waitFor(() => expect(result.current.sample).toBe(true)); await waitFor(() => expect(result.current.loading).toBe(false)); - expect(result.current.entries.length).toBe(DEMO_ON_CALL_ENTRIES.length); + expect(result.current.entries.length).toBe(EXAMPLE_SET_SIZE); expect(result.current.demoMode).toBe(true); expect(result.current.signedOut).toBe(false); expect(result.current.cachedAt).toBeNull(); diff --git a/tests/signed-out-example-switch.dom.test.tsx b/tests/signed-out-example-switch.dom.test.tsx new file mode 100644 index 0000000000..34efbea9cb --- /dev/null +++ b/tests/signed-out-example-switch.dom.test.tsx @@ -0,0 +1,41 @@ +/** @vitest-environment jsdom */ + +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { SignedOutExampleSwitch } from "@/components/mode-kit/signed-out-example-switch"; + +const sample = vi.hoisted(() => ({ active: true })); +vi.mock("@/components/mode-kit/use-signed-out-sample", () => ({ + useSignedOutSample: () => sample.active, +})); + +afterEach(() => { + cleanup(); + sample.active = true; +}); + +describe("SignedOutExampleSwitch", () => { + function renderSwitch() { + render( + Example

    } + notice={

    Sign in

    } + />, + ); + } + + it("shows the example while example data shows in the area", () => { + renderSwitch(); + expect(screen.getByTestId("example")).toBeTruthy(); + expect(screen.queryByTestId("notice")).toBeNull(); + }); + + it("shows the sign-in notice once example data is off", () => { + sample.active = false; + renderSwitch(); + expect(screen.getByTestId("notice")).toBeTruthy(); + expect(screen.queryByTestId("example")).toBeNull(); + }); +}); From ac4959808e929a207010f4db9b2a249def493715 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 06:22:15 +0000 Subject: [PATCH 2/3] Refresh the repo awareness snapshot for the new decision record Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CouthuMe2Xb6tBZ2Rk718N --- data/repo-awareness-snapshot.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/data/repo-awareness-snapshot.json b/data/repo-awareness-snapshot.json index 1cd386d203..63e3debb58 100644 --- a/data/repo-awareness-snapshot.json +++ b/data/repo-awareness-snapshot.json @@ -2901,6 +2901,11 @@ "section": "organisation", "catalogued": true }, + { + "path": "docs/decisions/2026-10-27-signed-out-examples-on-every-work-page.md", + "section": "decisions", + "catalogued": true + }, { "path": "docs/decisions/2026-10-19-rag-ts-split-deferred-until-after-launch.md", "section": "decisions", From 9cb07c7fbc5549c7755a44b1202efc02f8394496 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 06:39:02 +0000 Subject: [PATCH 3/3] Keep the CPD and credential pack examples look-only The signed-out CPD sample offers only the year it shows, and its Customise page is inert so it cannot change the device's layout. The example credential pack no longer prints. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CouthuMe2Xb6tBZ2Rk718N --- .../new-job/admin-credential-pack-page.tsx | 7 +++--- src/components/cme/cme-signed-out-sample.tsx | 22 ++++++++----------- tests/admin-credential-pack-page.dom.test.tsx | 2 ++ tests/cme-signed-out-sample.dom.test.tsx | 7 ++++++ 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/components/admin/new-job/admin-credential-pack-page.tsx b/src/components/admin/new-job/admin-credential-pack-page.tsx index 6a988680ea..d60f32d70c 100644 --- a/src/components/admin/new-job/admin-credential-pack-page.tsx +++ b/src/components/admin/new-job/admin-credential-pack-page.tsx @@ -206,12 +206,13 @@ export function AdminCredentialPackPage({ now: nowProp }: { now?: Date } = {}) {
- {/* Only this section prints: the shared print rule hides the app around `data-print-output`. */} + {/* Only this section prints: the shared print rule hides the app around `data-print-output`. + The example pack never prints: a browser Print would otherwise skip the Save as PDF guard. */}
Preview diff --git a/src/components/cme/cme-signed-out-sample.tsx b/src/components/cme/cme-signed-out-sample.tsx index 856595aaca..dfe257f0c8 100644 --- a/src/components/cme/cme-signed-out-sample.tsx +++ b/src/components/cme/cme-signed-out-sample.tsx @@ -59,8 +59,6 @@ const ENTRIES = DEMO_CME_ENTRIES; const ROUTINES = DEMO_CME_ROUTINES; const GOALS = DEMO_CME_PLAN_GOALS; const SAMPLE_YEARS = [DEMO_CME_YEAR.year] as const; -/** The same two years the demo loader offers (`load-cme-page-data`), so the year chips match. */ -const SAMPLE_AVAILABLE_YEARS = [DEMO_CME_YEAR.year, DEMO_CME_YEAR.year - 1]; /** Pages that moved to Admin: the server redirect does not run here, so follow it in the browser. */ const MOVED_TO_ADMIN: Readonly> = { @@ -183,13 +181,7 @@ function SampleBody({ pathname, query }: { readonly pathname: string; readonly q } if (pathname === "/cme/cpd-home") { return ( - + ); } if (pathname === "/cme/evidence") { @@ -201,7 +193,7 @@ function SampleBody({ pathname, query }: { readonly pathname: string; readonly q key={`${category}:${status}:${order}`} entries={ENTRIES} year={SET.year} - years={SAMPLE_AVAILABLE_YEARS} + years={SAMPLE_YEARS} demoMode initialCategory={category} initialStatus={status} @@ -214,7 +206,7 @@ function SampleBody({ pathname, query }: { readonly pathname: string; readonly q ; + // Look only: the layout is kept on this device, so the sample must not change it for whoever signs in next. + return ( +
+ +
+ ); } if (pathname === "/cme/learning") { // The public WA course list, the same for everyone; the home label is the sample year's. diff --git a/tests/admin-credential-pack-page.dom.test.tsx b/tests/admin-credential-pack-page.dom.test.tsx index 90db1253da..704add6413 100644 --- a/tests/admin-credential-pack-page.dom.test.tsx +++ b/tests/admin-credential-pack-page.dom.test.tsx @@ -83,6 +83,8 @@ describe("AdminCredentialPackPage", () => { expect(preview).toContain("Medical registration renewal"); // Whatever this device holds is never shown in the example. expect(preview).not.toContain("MED1111111111"); + // A browser Print must not output the example pack. + expect(screen.getByTestId("admin-credential-pack-preview").hasAttribute("data-print-output")).toBe(false); expect(screen.queryByTestId("admin-credential-pack-signed-out")).toBeNull(); }); diff --git a/tests/cme-signed-out-sample.dom.test.tsx b/tests/cme-signed-out-sample.dom.test.tsx index afdfa737fc..3c52f18aa6 100644 --- a/tests/cme-signed-out-sample.dom.test.tsx +++ b/tests/cme-signed-out-sample.dom.test.tsx @@ -127,6 +127,13 @@ describe("CPD signed-out sample", () => { expect(screen.getByTestId("cme-sample-unavailable").textContent).toContain("not part of the example data"); }); + it("shows Customise look-only, so the sample cannot change this device's layout", async () => { + nav.pathname = "/cme/customise"; + renderBoundary(); + await sampleShown(); + expect(screen.getByTestId("cme-sample-customise").hasAttribute("inert")).toBe(true); + }); + it("follows Job applications to Admin, where it moved", async () => { nav.pathname = "/cme/applications/cv"; renderBoundary();