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();