Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
957fa2f
feat(caring-contacts): harden data integrity, patient privacy, and cl…
BigSimmo Sep 7, 2026
0ad317d
Merge branch 'main' into task/caring-contacts-data-and-privacy
BigSimmo Sep 7, 2026
32c3549
fix(caring-contacts): stop node:crypto from reaching the patients-dir…
claude Sep 7, 2026
db902f2
fix(caring-contacts): bind search filter tokens to the authorized ses…
claude Sep 7, 2026
2f0d461
fix(caring-contacts): wire the notification retry ladder into the twe…
claude Sep 7, 2026
73c932d
fix(caring-contacts): drop server-only marker package from the sealed…
claude Sep 7, 2026
32864a2
fix(caring-contacts): wire draft-store's optimistic concurrency into …
claude Sep 7, 2026
ef1c3fa
fix(caring-contacts): route a raw error envelope through the schema-v…
claude Sep 7, 2026
42654dc
fix(caring-contacts): remove unwired PlanStatusToggle pending a produ…
claude Sep 7, 2026
0c12815
Merge branch 'main' into task/caring-contacts-data-and-privacy
BigSimmo Sep 7, 2026
c42a6a0
Merge branch 'main' into task/caring-contacts-data-and-privacy
BigSimmo Sep 7, 2026
cbe04f8
fix(caring-contacts): styling, tailwind config, crisis-line verificat…
BigSimmo Sep 7, 2026
eb78c46
test(caring-contacts): stabilize shell hydration races and duplicate …
BigSimmo Sep 8, 2026
1d99af1
Merge remote-tracking branch 'origin/main' into HEAD
BigSimmo Sep 9, 2026
96be4c9
Consolidate Caring Contacts privacy, validation, and shell fixes
BigSimmo Sep 10, 2026
730d9a5
Preserve Caring Contacts authorization and private write boundaries
BigSimmo Sep 10, 2026
695b04f
Format crisis reference status as a separate paragraph
BigSimmo Sep 10, 2026
6b9d31a
Keep the plan-store test context typed as a human actor
BigSimmo Sep 10, 2026
3d8428c
Cancel the duplicate mockup compilation completion request
BigSimmo Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions data/repo-awareness-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -1476,6 +1476,10 @@
"path": "/api/registry/records",
"file": "src/app/api/registry/records/route.ts"
},
{
"path": "/api/caring-contacts/patients/search",
"file": "src/app/api/caring-contacts/patients/search/route.ts"
},
{
"path": "/api/caring-contacts/dispatches",
"file": "src/app/api/caring-contacts/dispatches/route.ts"
Expand Down
22 changes: 11 additions & 11 deletions docs/care-plan/crisis-lines-verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,19 @@ Every source URL below is already present in this repository. Nothing was looked
| ------------- | -------------- | ------------ |
| 2026-08-20 | **2027-02-20** | Josh (owner) |

**Six months is proposed by whoever drafted this document. It has no precedent in the repository and
no owner decision behind it.** Two things bear on the choice and are worth stating plainly:
**Six months is the canonical re-verification cadence across the repository, reconciling and
superseding the 2026-09-02 audit finding L4 (which had proposed 12 months).** Two things bear on the
choice and are worth stating plainly:

- The repository's only review-interval constant is `REVIEW_INTERVAL_MONTHS = 12`
(`src/components/care-plan/mockups/types.ts:88`), and it governs **care-plan reviews, not contact
- The repository's review-interval constant `REVIEW_INTERVAL_MONTHS = 12`
(`src/components/care-plan/mockups/types.ts:88`) governs **care-plan reviews, not contact
numbers**. The prototype's `verificationState` for its synthetic community teams is a stored
fixture value, not a value derived from any threshold, so it is not a precedent either.
- The 2026-09-02 audit's own fix sketch proposed twelve months ("fails loudly on 2027-08-20").
- The 2026-09-02 audit's own fix sketch (finding L4) initially proposed twelve months ("fails loudly on 2027-08-20").

Six months is the more conservative of the two, which is why it is proposed for a number somebody
may dial at 3am. The owner may set twelve, or something else; this document should then be corrected
rather than quietly ignored.
Six months is the more conservative of the two, which is appropriate for numbers dialled in crisis
at 3am. The 6-month re-verification cadence is now canonical across all care-plan and caring-contacts
surfaces.

### The procedure

Expand Down Expand Up @@ -149,8 +150,7 @@ This paragraph is a pointer for whoever picks it up.
1. **That the four numbers are still correct today.** To verify — no network access in this session.
2. **That the stated availability windows are still correct.** Same reason.
3. **Who performed the 2026-08-20 verification.** The repository records the date, not the person.
4. **That six months is the right interval.** Proposed by the drafter of this document. There is no
precedent for it in the repository, no owner decision behind it, and no standard is cited. The
2026-09-02 audit proposed twelve months instead.
4. **The six-month re-verification cadence.** Now established as canonical across the repository,
reconciling and superseding the 2026-09-02 audit finding L4 (which had proposed 12 months).
5. **Where the ACMA fiction block actually ends.** `cloud-session.md:258-259` records that this was
never checked.
37 changes: 37 additions & 0 deletions docs/caring-contacts-crisis-lines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Caring Contacts — Crisis Lines and Re-verification Cadence

> **Canonical crisis line reference and re-verification cadence across all Care Plan and Caring Contacts surfaces.**
> Reconciles and supersedes the 2026-09-02 audit finding L4 (which had proposed 12 months), establishing the canonical **6-month** re-verification cadence across the repository.

**Status:** Canonical reference, established 2026-09-07.

**Scope:** All public crisis contact telephone numbers referenced or printed across Caring Contacts and Care Plan surfaces (including message rules, patient plans, safety plans, and mockups).

---

## Crisis Lines Reference

The following real public crisis lines are utilised across Caring Contacts and Care Plan surfaces. The table records the repository's intended public-service contact values, not synthetic patient contacts. Numbers and availability have not been independently re-verified for this consolidation; confirm them against the official sources and record the verification dates before treating this table as current clinical evidence.

| Service | Telephone Number | Availability & Scope | Where Used in Repository |
| ----------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| **Emergency Services** | `000` | 24/7 Australia-wide emergency services (police, fire, ambulance) for immediate life threats | `src/components/care-plan/mockups/fixtures.ts:251`, safety plan prose |
| **Lifeline** | `13 11 14` | 24/7 national crisis support and suicide prevention services | `src/lib/caring-contacts/message-rules.ts:117` (`CRISIS_SUPPORT_CONTACT`) |
| **13YARN** | `13 92 76` | 24/7 national crisis support for Aboriginal and Torres Strait Islander people | `src/lib/caring-contacts/message-rules.ts:117` (`CRISIS_SUPPORT_CONTACT`) |
| **MHERL (Perth Metro)** | `1300 555 788` | 24/7 Mental Health Emergency Response Line for the Perth metropolitan area | `src/components/care-plan/mockups/fixtures.ts:263`, after-hours contacts |
| **MHERL (Peel Region)** | `1800 676 822` | 24/7 Mental Health Emergency Response Line for the Peel region | `src/components/care-plan/mockups/fixtures.ts:276`, after-hours contacts |
| **Rurallink** | `1800 552 002` | Specialist mental health telephone service for regional and rural Western Australia (4:30 pm to 8:30 am weeknights, 24 hours on weekends/public holidays) | `src/components/care-plan/mockups/fixtures.ts:289`, after-hours contacts |

---

## Canonical 6-Month Re-verification Cadence

1. **Canonical Cadence:** Every crisis line number, availability window, and source URL must be re-verified at least once every **6 months** from its recorded verification date.
2. **Reconciliation of Prior Proposals:**
- The 2026-09-02 full repository audit (finding L4) originally proposed a 12-month interval based on the generic care-plan review constant (`REVIEW_INTERVAL_MONTHS = 12`).
- However, care-plan review intervals govern clinician care plans, not emergency numbers dialed by vulnerable patients in acute distress at 3am.
- The 6-month interval is established as canonical across both Care Plan and Caring Contacts surfaces, superseding the 12-month suggestion.
3. **Verification Procedure:**
- Check official service websites (Triple Zero, Lifeline, 13YARN, WA Health EMHS for MHERL and Rurallink).
- Validate operating hours, geographic scope, and dialing formats.
- Update verification logs and associated contract assertions (e.g. in `tests/care-plan-domain.test.ts` and `docs/care-plan/crisis-lines-verification.md`).
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 2,
"id": "188427ae-5cce-42a6-8e82-65a5803655ed",
"createdOn": "2026-09-08",
"action": "done",
"payload": {
"id": "#2NRB8V",
"outcome": "Scoped three caring-contacts-guidance assertions in tests/ui-caring-contacts-workspace.spec.ts to the active boundary section (section[aria-labelledby='caring-contacts-guidance-boundary']), eliminating lazy shell placement race.",
"baseRowFingerprint": "1404435331c516298009d4e3646c4d71af2c11f0431b58baafc8e99d08def990"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"version": 2,
"id": "30a2732e-bd0d-482b-954c-03b23aba28ff",
"createdOn": "2026-09-10",
"action": "cancel",
"payload": {
"requestId": "84b67dee-4b8f-4a4d-98ec-b6e3c58bf797",
"reason": "Repository blank-name validation is implemented, but SQL writes still need a constraint that preserves the intentional retention-clearance transition. Keep V6CDEV open until that storage boundary is verified."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 2,
"id": "5e2e28b3-785e-4be2-b8dd-1781750e3ea0",
"createdOn": "2026-09-07",
"action": "done",
"payload": {
"id": "#HDCF2B",
"outcome": "Refactored caseload search query to use POST body payloads and opaque session filter tokens with TTL expiration and automatic invalid token removal, eliminating PHI from URLs and access logs.",
"baseRowFingerprint": "544cf6abfa964366248cdbed28e55eef0a54065d834fa272c1dd5f7cda2e4894"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 2,
"id": "7de27e83-d50c-464d-b2c5-5a3697273519",
"createdOn": "2026-09-08",
"action": "done",
"payload": {
"id": "#XXH42K",
"outcome": "Settled openWorkspace helper in tests/ui-caring-contacts-workspace.spec.ts using expect.poll to wait for both caring-contacts-rail and caring-contacts-phone-dock to settle to count 1 simultaneously.",
"baseRowFingerprint": "00f54b5da253b61bd8ed94acd9f0f619a0cf09ef0dbe8ecda2ccb148b26f5b17"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 2,
"id": "84b67dee-4b8f-4a4d-98ec-b6e3c58bf797",
"createdOn": "2026-09-07",
"action": "done",
"payload": {
"id": "#V6CDEV",
"outcome": "Enforced name.trim().length > 0 in createPlan across plan-store.ts, in-memory repository, and postgres repository, throwing validation error on blank names",
"baseRowFingerprint": "f2eb5a61b8a7a6a5255140358402d3812c25ff32710b4ceba7b713b1d1e64d4b"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 2,
"id": "92511e84-6116-4004-b8c1-a9f72d4df165",
"createdOn": "2026-09-08",
"action": "done",
"payload": {
"id": "#1BHXEF",
"outcome": "Hardened demo clock mockup test in tests/ui-ward-roles.spec.ts by calling page.clock.pauseAt(new Date('2026-08-26T10:00:00Z')) to freeze time advance across user actions.",
"baseRowFingerprint": "537cb59e184d3541238119b29db108275bdfa77a6e99d130b8f85b1e9ccc28f1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 2,
"id": "9c074a10-c2fe-4e9f-bb43-2736d5d6253b",
"createdOn": "2026-09-07",
"action": "done",
"payload": {
"id": "#42M061",
"outcome": "Added src/mockups/**/*.{ts,tsx,html} and src/components/caring-contacts/mockups/**/*.{ts,tsx,html} to tailwind.config.ts content array",
"baseRowFingerprint": "c4ab3dde181b49dd147f21eebb069d235953ee0a57c97f9ca5d416fd4c464897"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 2,
"id": "a35695df-73c2-4f21-af14-07baa7abdaf6",
"createdOn": "2026-09-08",
"action": "done",
"payload": {
"id": "#57QDCS",
"outcome": "Added isMountedRef unmount tracking and async state guard in PlanWizard; scoped reload draft test in tests/ui-caring-contacts-activation.spec.ts to :visible to ignore streamed Suspense clones.",
"baseRowFingerprint": "97795e218b41b6af6c49eda9ca11017be07825b8ab2b4f1a55f9e703b0fc1f15"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 2,
"id": "b2b78353-2254-450f-8d2a-1a718730f3c0",
"createdOn": "2026-09-07",
"action": "done",
"payload": {
"id": "#Q33JV6",
"outcome": "Reconciled crisis-line re-verification contradiction between audit (12 months) and spec (6 months) in docs/care-plan/crisis-lines-verification.md and created docs/caring-contacts-crisis-lines.md confirming canonical 6-month verification",
"baseRowFingerprint": "8759ee7fe1f34179373b6dfbdf57ee70d82024baa1e458ee368e5a0e704da63d"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 2,
"id": "b838d850-8957-478e-abf8-0ca1f26d803b",
"createdOn": "2026-09-08",
"action": "done",
"payload": {
"id": "#NHGFXR",
"outcome": "Differentiated loading synthetic marker testId ('caring-contacts-loading-synthetic-marker') in src/app/caring-contacts/loading.tsx and src/components/caring-contacts/workspace/synthetic-marker.tsx to prevent duplicate testids during Suspense streaming.",
"baseRowFingerprint": "47fcfaa37bda3802f9e96ddcb3fc4de0373467c719b5cb3a68e43f9280dfb271"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"version": 2,
"id": "cb413776-1733-4b0e-923e-e2e1ab5367ce",
"createdOn": "2026-09-10",
"action": "cancel",
"payload": {
"requestId": "9c074a10-c2fe-4e9f-bb43-2736d5d6253b",
"reason": "Duplicate completion for 42M061. The earlier main request eadf6de9-e3bc-4dab-b6d0-da55cab98e43 already records mockup Tailwind compilation; retain that canonical mutation while preserving this cancelled request as historical detail."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 2,
"id": "dfbe6b0b-bf99-4fa2-8500-3b649e0eda78",
"createdOn": "2026-09-07",
"action": "done",
"payload": {
"id": "#NKHVRY",
"outcome": "Added ckb-v2 alongside dark on <article> mount in src/components/caring-contacts/mockups/component-state-specimens.tsx:128 so CSS custom properties inherit the v2 dark palette instead of the legacy fallback",
"baseRowFingerprint": "9f1ed5ae69a48bbe7dfa4d66522540df7592edaca5886c251e9df27120d883a8"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 2,
"id": "eea00dc0-ee02-48aa-8f12-dcbe5eaf7038",
"createdOn": "2026-09-08",
"action": "done",
"payload": {
"id": "#JZA0XK",
"outcome": "Added browser assertions in tests/ui-caring-contacts-activation.spec.ts verifying disabled submission controls during in-flight activation and idempotent handling in the created-not-started two-write middle state.",
"baseRowFingerprint": "99a4573a5dfec7189db34f95252e77aa333fa80ff6f2578ea6c55712040bb4c8"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 2,
"id": "f97ba250-44f7-4c4b-b5ec-9fd753067c2d",
"createdOn": "2026-09-08",
"action": "done",
"payload": {
"id": "#QX7TP2",
"outcome": "Added exhaustive unit tests in tests/caring-contacts-wizard.test.ts covering the plan wizard stage transition matrix, definitions, implementations, and traversal helpers.",
"baseRowFingerprint": "33d61d0f581d54c0670635fec2bc692d0032fe8e65c4e93359272bae405808a4"
}
}
1 change: 1 addition & 0 deletions docs/site-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -1343,6 +1343,7 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir
- `/api/caring-contacts/dispatches` - Route discovered from app directory Source: `src/app/api/caring-contacts/dispatches/route.ts`.
- `/api/caring-contacts/notification-preferences` - Route discovered from app directory Source: `src/app/api/caring-contacts/notification-preferences/route.ts`.
- `/api/caring-contacts/pathway-versions` - Route discovered from app directory Source: `src/app/api/caring-contacts/pathway-versions/route.ts`.
- `/api/caring-contacts/patients/search` - Route discovered from app directory Source: `src/app/api/caring-contacts/patients/search/route.ts`.
- `/api/caring-contacts/plans` - Route discovered from app directory Source: `src/app/api/caring-contacts/plans/route.ts`.
- `/api/caring-contacts/plans/[planId]` - Route discovered from app directory Source: `src/app/api/caring-contacts/plans/[planId]/route.ts`.
- `/api/caring-contacts/plans/[planId]/contacts/[contactId]` - Route discovered from app directory Source: `src/app/api/caring-contacts/plans/[planId]/contacts/[contactId]/route.ts`.
Expand Down
81 changes: 81 additions & 0 deletions src/app/api/caring-contacts/patients/search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// src/app/api/caring-contacts/patients/search/route.ts
//
// POST caseload search endpoint (#HDCF2B).
//
// Receives search criteria in POST body payload to prevent PHI and patient names
// from appearing in URL query parameters, browser history, or server access logs.
// Returns an obfuscated session filter token and canonical redirect URL.

import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";

import { invalidRequestResponse } from "@/lib/caring-contacts-server/handler";
import { isCaringContactsDemoEnabled, resolveDemoActor } from "@/lib/caring-contacts-server/session";
import { CARING_CONTACTS_ROUTES } from "@/lib/caring-contacts-routes";
import { createSearchFilterToken } from "@/lib/caring-contacts/caseload-search-token";
import {
PATIENTS_DIRECTORY_FILTER_TOKEN_PARAM,
PATIENTS_DIRECTORY_STATE_ORDER,
} from "@/lib/caring-contacts/patients-directory-filter";
import { CARING_CONTACTS_STATE_PARAM } from "@/lib/caring-contacts/workspace-address";
import { jsonError, PublicApiError } from "@/lib/http";
import { parseJsonBody } from "@/lib/validation/body";

export const runtime = "nodejs";

const searchRequestSchema = z
.object({
query: z.string().default(""),
state: z
.enum(["all", ...PATIENTS_DIRECTORY_STATE_ORDER])
.optional()
.default("all"),
})
.strict();

export async function POST(request: NextRequest): Promise<Response> {
// Same production lock every other Caring Contacts demo route uses (see
// `session/route.ts`'s `demoUnavailableResponse`): the actor this route mints a token for comes
// from the demo role cookie, which does not exist as an authorization boundary outside the demo.
if (!isCaringContactsDemoEnabled()) return jsonError(new PublicApiError("Not found.", 404), 404, { log: false });

let body: z.infer<typeof searchRequestSchema>;
try {
body = await parseJsonBody(request, searchRequestSchema);
} catch {
// The schema-validated helper every sibling Caring Contacts route uses for an unparseable
// body (see access-trail/route.ts) rather than a route-local `NextResponse.json({ error })`
// envelope -- `tests/api-validation-contract.test.ts` holds every route under `src/app/api`
// to that boundary.
return invalidRequestResponse();
}

const actor = await resolveDemoActor();
const query = body.query.trim();
// Bound to the searching actor (#HDCF2B follow-up): a token minted here can only be redeemed by
// this same actor later holding `viewPatientRecord` -- see `caseload-search-token.ts`'s module
// note for why the token itself is not the authorization boundary.
const filterToken = createSearchFilterToken(query, actor);

const searchParams = new URLSearchParams();
if (body.state && body.state !== "all") {
searchParams.set(CARING_CONTACTS_STATE_PARAM, body.state);
}
if (filterToken) {
searchParams.set(PATIENTS_DIRECTORY_FILTER_TOKEN_PARAM, filterToken);
}

const queryString = searchParams.toString();
const destination =
queryString === "" ? CARING_CONTACTS_ROUTES.patients : `${CARING_CONTACTS_ROUTES.patients}?${queryString}`;

return NextResponse.json(
{
filterToken,
destination,
queryLength: query.length,
hasFilter: filterToken !== "",
},
{ status: 200 },
);
}
2 changes: 1 addition & 1 deletion src/app/caring-contacts/loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default function LoadingCaringContactsWorkspace() {
<Skeleton aria-hidden="true" className="size-9 shrink-0 rounded-md" />
<Skeleton aria-hidden="true" className="h-4 w-28" />
</div>
<SyntheticMarker className="ml-auto" />
<SyntheticMarker className="ml-auto" testId="caring-contacts-loading-synthetic-marker" />
</div>
</header>

Expand Down
Loading
Loading