Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
80b6133
Work: one "Get set up" checklist on My Day, no per-area setup rows (D11)
claude Oct 13, 2026
00b7bed
Work pages: use the standard status shapes (D04)
claude Oct 13, 2026
8669200
Honest review labels: sign-off shown only for a named person and date…
claude Oct 13, 2026
af077db
Crisis numbers: one shared card on the answer page and Admin Help, pl…
claude Oct 13, 2026
d0f61a5
CPD and End of year: one coloured button per screen (D08)
claude Oct 13, 2026
e62e904
CPD Log: one docked button, no floating Copy next (D09)
claude Oct 13, 2026
91cf056
CPD Log: test that the dock holds one button (D09)
claude Oct 13, 2026
4bab692
MHA clock: plain list, Start clock from form records, renamed MHA tim…
claude Oct 13, 2026
9812afd
Work: shared state card on Open shifts, Manage team and Notifications…
claude Oct 13, 2026
ee1491f
Answer source marker: invisible 48px tap area, same look and text flo…
claude Oct 13, 2026
cc01157
Answer page: key points and answer actions wrap at 320px large text (…
claude Oct 13, 2026
4fa6aec
Roster and My Day: the design system's shift codes (D12)
claude Oct 13, 2026
7ce8981
Phone menu: default shortcuts become the urgent tools
claude Oct 13, 2026
c46ddfe
My Day: hold Today's skeleton until its own reads answer (D16)
claude Oct 13, 2026
f88fdcd
Large text at 320 px: wrap filter rows and segments (D17)
claude Oct 13, 2026
d600fb9
Work pills in sentence case (D20)
claude Oct 13, 2026
7325846
Roster Month: the next-shift line wraps instead of cutting off (D23)
claude Oct 13, 2026
b911f00
Privacy: plain words, plain date, no coloured side bars, sentence cas…
claude Oct 13, 2026
3760367
Say repeated lines once on Work pages (D24)
claude Oct 13, 2026
545c3f1
End of year: the domains count is 14 px page text, not 8 px (D25)
claude Oct 13, 2026
5840b80
Clinical questions: hold back patient details before sending, on the …
claude Oct 13, 2026
b51ffda
Answer sources: a vertical list on phones; library rows open on a tap…
claude Oct 13, 2026
0987d04
Merge branch 'design/work-d11-d13-d16-d22' into claude/project-thread…
claude Oct 13, 2026
c53a305
Keep the retired setup row for 30 days
claude Oct 13, 2026
cf9c08d
Answer page: a plain 'Not in your sources' state (D03)
claude Oct 13, 2026
66fa067
Merge branch 'work-design-findings-d04-d25' into claude/project-threa…
claude Oct 13, 2026
31f1001
Work and Clinical bands: drop the date line except on My Day, Roster,…
claude Oct 13, 2026
cb6e5e1
Work page titles name the page (audit D15)
claude Oct 13, 2026
12270c7
Answer page: one quiet line on where a question goes (S3)
claude Oct 13, 2026
7814f4f
Roster: the L shift reads Late, matching the design-system codes
claude Oct 13, 2026
156b2b5
Format roster calendar test
claude Oct 13, 2026
cf4d0ce
Work band corner is the Favourite heart only (audit D18)
claude Oct 13, 2026
6c7e34c
Work band: the line above the title wraps instead of cutting off (aud…
claude Oct 13, 2026
d253961
Assessments and Manage team keep their parent's area pill (audit D21)
claude Oct 13, 2026
e84774d
Band titles never cut off at 320 px with large text
claude Oct 13, 2026
2aa1dc4
Merge branch 'worktree-agent-a8a685f23c1646a8f' into claude/project-t…
claude Oct 13, 2026
2af2274
Clinical mode tabs: every tab is at least 48 px wide to tap
claude Oct 13, 2026
84cf5c4
Renewals and Admin Today: counts as words, one month-grouped list
claude Oct 13, 2026
107af33
Offline page: urgent numbers first, privacy line last
claude Oct 13, 2026
8073307
Merge branch 'worktree-agent-a28b0e15d52544192' into claude/project-t…
claude Oct 13, 2026
0348e32
Format answer notice test
claude Oct 13, 2026
2003d1c
fix: keep search header within its size budget and use the pill radiu…
claude Oct 13, 2026
dcd4d1e
fix: let QTc millisecond readings, MS and 'pt with' through the clini…
claude Oct 13, 2026
a29b692
fix: keep the title Ms when a number sits before it in a clinical que…
claude Oct 13, 2026
e3acc79
test: count the overseas note as answer content and expect the phone …
claude Oct 13, 2026
289da3d
Answer page: keep the original phone bottom padding
claude Oct 13, 2026
c67552e
test: measure the overseas note with the gap above it
claude Oct 13, 2026
2f4e138
Answer actions: keep the row on one line, let the Copy label give way
claude Oct 13, 2026
15fefd3
fix: Validate queries in summary mode
qodo-code-review[bot] Oct 13, 2026
828917f
fix: Detect bare patient names near clinical content
qodo-code-review[bot] Oct 13, 2026
0f218da
fix: 2 findings — Guard all answer-mode searches; Show crisis cards for
qodo-code-review[bot] Oct 13, 2026
0d8d904
fix: Report the true searched-source count
qodo-code-review[bot] Oct 13, 2026
f27a261
fix: Reject future-dated sign-offs
qodo-code-review[bot] Oct 13, 2026
9c05c23
Revert "fix: Detect bare patient names near clinical content"
claude Oct 13, 2026
9be8b19
Format sign-off-label.ts
BigSimmo Oct 13, 2026
8d46b28
Merge remote-tracking branch 'origin/main' into sw1013-project-thread…
BigSimmo Oct 13, 2026
1ae37de
Load the Clinical patient-detail check only when a question is typed
claude Oct 13, 2026
d56c753
Merge branch 'claude/project-thread-82q41w' of https://github.com/Big…
claude Oct 13, 2026
a7bb5bc
Let the composer tests wait for the lazily loaded patient check
claude Oct 13, 2026
917e9fd
Reset the Roster shifts and Assessments size baselines to current main
claude Oct 13, 2026
f331d20
fix: Invalidate stale searches before patient checks
qodo-code-review[bot] Oct 13, 2026
7cbb610
fix: Load patient checks only for Answer mode
qodo-code-review[bot] Oct 13, 2026
02da356
Keep a stale question from taking over, without cancelling the search…
claude Oct 13, 2026
ff18674
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
ba47f41
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
827cd7b
Move the composer's patient check into its own hook to keep the heade…
claude Oct 13, 2026
4d2bf96
Merge remote-tracking branch 'origin/claude/project-thread-82q41w' in…
claude Oct 13, 2026
04782a4
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
13326e7
Audit review follow-ups: bed number with sex word, AL legend, CPD fil…
claude Oct 13, 2026
3d80891
Patient check: keep marked and other place-word numbers out of the ag…
claude Oct 13, 2026
bcd0195
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
33ad881
Review follow-ups: calendar action in Other services, cross-year rene…
BigSimmo Oct 13, 2026
2b8cd05
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
2dd15a1
Merge remote-tracking branch 'origin/claude/project-thread-82q41w' in…
BigSimmo Oct 13, 2026
c79ae87
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
b3eb0a1
Owner decisions 13 Oct: lowercase ms before capitalised terms is a re…
BigSimmo Oct 13, 2026
2080749
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
65de42d
Merge remote-tracking branch 'origin/claude/project-thread-82q41w' in…
BigSimmo Oct 13, 2026
f7cbda9
Answer home: show the overseas-processing notice before the first que…
BigSimmo Oct 13, 2026
6d50ce4
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
c5087e0
Merge remote-tracking branch 'origin/claude/project-thread-82q41w' in…
BigSimmo Oct 13, 2026
56de0f1
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
b8e076d
Merge remote-tracking branch 'origin/claude/project-thread-82q41w' in…
BigSimmo Oct 13, 2026
9d91152
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
1c0d180
Merge remote-tracking branch 'origin/claude/project-thread-82q41w' in…
BigSimmo Oct 13, 2026
9a35daa
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
35dbeec
Merge remote-tracking branch 'origin/claude/project-thread-82q41w' in…
BigSimmo Oct 13, 2026
4e7d95d
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
977fbfe
Merge remote-tracking branch 'origin/claude/project-thread-82q41w' in…
BigSimmo Oct 13, 2026
0616277
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
0d710c4
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
5d04c9b
patient check keeps place numbers that look like ms readings; Answer …
BigSimmo Oct 13, 2026
1d5d438
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
7a23b1f
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
10295ff
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
ae7d348
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
c335f9d
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
a9a95f5
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
6ce6de6
fix(clinical-question): keep capitalised place words (RM, CHAIR) from…
BigSimmo Oct 13, 2026
512d571
Merge remote-tracking branch 'origin/main' into gb-ps3519-0640
BigSimmo Oct 13, 2026
9722b15
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 2026
325d47c
Merge branch 'main' into claude/project-thread-82q41w
BigSimmo Oct 13, 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
6 changes: 3 additions & 3 deletions bundle-budget.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$comment": "Client JS bundle-size budgets captured from a known-good production build. `production` guards aggregate user-facing chunks, `routes` guards the Lighthouse journeys and the main work-mode pages (added by the work-mode speed review, 7 Oct 2026) against route-local growth (route baselines lowered about 100 KB on 7 Oct 2026 (scaled from a local build by the ratio to the baselineSource build, so they stay comparable with it) when the sign-in library moved out of the first load, so that saving cannot quietly regrow), and `mockups` is a looser design-scratch hygiene ceiling. The `production` baseline alone was raised on 9 Oct 2026 to a local build of the hospital roles change merged with main (5472.8 KiB), after its 145 KiB duplicated-chunk regression was removed: main had already reached +9.8% from the combined work-mode growth. Route baselines and baselineSource were left as they were. Refresh intentionally with `npm run check:bundle-budget -- --update`.",
"$comment": "Client JS bundle-size budgets captured from a known-good production build. `production` guards aggregate user-facing chunks, `routes` guards the Lighthouse journeys and the main work-mode pages (added by the work-mode speed review, 7 Oct 2026) against route-local growth (route baselines lowered about 100 KB on 7 Oct 2026 (scaled from a local build by the ratio to the baselineSource build, so they stay comparable with it) when the sign-in library moved out of the first load, so that saving cannot quietly regrow), and `mockups` is a looser design-scratch hygiene ceiling. The `production` baseline alone was raised on 9 Oct 2026 to a local build of the hospital roles change merged with main (5472.8 KiB), after its 145 KiB duplicated-chunk regression was removed: main had already reached +9.8% from the combined work-mode growth. Route baselines and baselineSource were left as they were. Refresh intentionally with `npm run check:bundle-budget -- --update`. On 13 Oct 2026 the owner approved resetting two route baselines, /roster/shifts (390.9 KiB) and /teaching/assessments (459.3 KiB), to a local production build of main at f4bcb55a4 (which matched CI's own measurement of that commit), because main had already reached +9.7% and +8.9% there. All other baselines were left as they were.",
"enforce": true,
"production": {
"gzipBytes": 5604147,
Expand Down Expand Up @@ -42,7 +42,7 @@
"tolerancePct": 10
},
"/roster/shifts": {
"gzipBytes": 365000,
"gzipBytes": 400282,
"tolerancePct": 10
},
"/open-shifts": {
Expand All @@ -62,7 +62,7 @@
"tolerancePct": 10
},
"/teaching/assessments": {
"gzipBytes": 432014,
"gzipBytes": 470323,
"tolerancePct": 10
},
"/cme": {
Expand Down
6 changes: 3 additions & 3 deletions docs/design-system/COMPONENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1058,14 +1058,14 @@ This generated snapshot is a local source-derived inventory. It does not assert
| `AnswerFooter` | answer | yes | yes | inherited-global-root | yes | no | 1 |
| `AsyncButton` | controls | yes | yes | inherited-global-root | yes | no | 5 |
| `Breadcrumb` | layout | yes | yes | inherited-global-root | yes | no | 1 |
| `Button` | controls | yes | yes | inherited-global-root | yes | no | 129 |
| `Button` | controls | yes | yes | inherited-global-root | yes | no | 128 |
| `Checkbox` | controls | yes | yes | inherited-global-root | yes | no | 14 |
| `Chip` | controls | yes | yes | inherited-global-root | yes | no | 8 |
| `ChoiceChip` | controls | yes | yes | inherited-global-root | yes | no | 7 |
| `Citation` | source | yes | yes | inherited-global-root | yes | no | 1 |
| `CitationList` | source | yes | yes | inherited-global-root | yes | no | 1 |
| `ConfirmDialog` | layout | yes | yes | inherited-global-root | yes | no | 13 |
| `DateDisplay` | source | yes | yes | inherited-global-root | yes | no | 4 |
| `DateDisplay` | source | yes | yes | inherited-global-root | yes | no | 3 |
| `Disclosure` | layout | yes | yes | inherited-global-root | yes | no | 3 |
| `DisclosureGroup` | layout | yes | yes | inherited-global-root | yes | no | 1 |
| `DoseLine` | answer | yes | yes | no | yes | no | 0 |
Expand All @@ -1078,7 +1078,7 @@ This generated snapshot is a local source-derived inventory. It does not assert
| `FieldHint` | feedback | yes | yes | inherited-global-root | yes | no | 2 |
| `FormField` | controls | yes | yes | inherited-global-root | yes | no | 18 |
| `IconButton` | controls | yes | yes | inherited-global-root | yes | no | 3 |
| `InlineNotice` | feedback | yes | yes | inherited-global-root | yes | no | 33 |
| `InlineNotice` | feedback | yes | yes | inherited-global-root | yes | no | 32 |
| `LinkAction` | controls | yes | yes | no | yes | no | 0 |
| `LoadingPanel` | feedback | yes | yes | inherited-global-root | yes | no | 10 |
| `MissingValue` | feedback | yes | yes | inherited-global-root | yes | no | 11 |
Expand Down
6 changes: 0 additions & 6 deletions docs/design-system/adoption-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,6 @@
"src/components/roster/manage/roster-approve-tab.tsx",
"src/components/roster/manage/roster-cover-tab.tsx",
"src/components/roster/manage/roster-decision-sheet.tsx",
"src/components/roster/manage/roster-manage-page.tsx",
"src/components/roster/manage/roster-people-list.tsx",
"src/components/roster/manage/roster-team-settings.tsx",
"src/components/roster/requests/roster-dates-sheet.tsx",
Expand Down Expand Up @@ -509,7 +508,6 @@
"src/components/roster/manage/roster-approve-tab.tsx",
"src/components/roster/manage/roster-cover-tab.tsx",
"src/components/roster/manage/roster-decision-sheet.tsx",
"src/components/roster/manage/roster-manage-page.tsx",
"src/components/roster/manage/roster-people-list.tsx",
"src/components/roster/manage/roster-team-settings.tsx",
"src/components/roster/requests/roster-dates-sheet.tsx",
Expand Down Expand Up @@ -897,13 +895,11 @@
"entryExported": true,
"directImportFiles": [
"src/components/answer/AnswerFooter.tsx",
"src/components/privacy-quiet-signal-page.tsx",
"src/components/ui/retrieval-state-banner.tsx",
"src/components/ui/verification-notice.tsx"
],
"productImportFiles": [
"src/components/answer/AnswerFooter.tsx",
"src/components/privacy-quiet-signal-page.tsx",
"src/components/ui/retrieval-state-banner.tsx",
"src/components/ui/verification-notice.tsx"
],
Expand Down Expand Up @@ -1437,7 +1433,6 @@
"src/components/DocumentViewer.tsx",
"src/components/calendar/calendar-subscribe.tsx",
"src/components/clinical-dashboard/account-setup-dialog.tsx",
"src/components/clinical-dashboard/answer-crisis-banner.tsx",
"src/components/clinical-dashboard/auth-panel.tsx",
"src/components/clinical-dashboard/email-auth-form.tsx",
"src/components/clinical-dashboard/medication-considerations.tsx",
Expand Down Expand Up @@ -1473,7 +1468,6 @@
"src/components/DocumentViewer.tsx",
"src/components/calendar/calendar-subscribe.tsx",
"src/components/clinical-dashboard/account-setup-dialog.tsx",
"src/components/clinical-dashboard/answer-crisis-banner.tsx",
"src/components/clinical-dashboard/auth-panel.tsx",
"src/components/clinical-dashboard/email-auth-form.tsx",
"src/components/clinical-dashboard/medication-considerations.tsx",
Expand Down
8 changes: 7 additions & 1 deletion knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
"*.config.{ts,mts,mjs,cjs}"
],
"project": ["src/**/*.{ts,tsx}", "scripts/**/*.{ts,mjs,cjs}", "worker/**/*.ts", "eslint-rules/**/*.mjs"],
"ignore": ["src/lib/supabase/database.types.ts", "**/*mockup*", "supabase/functions/**", "docs/**"],
"ignore": [
"src/lib/supabase/database.types.ts",
"**/*mockup*",
"supabase/functions/**",
"docs/**",
"src/components/work-setup/work-setup-row.tsx"
],
"ignoreDependencies": [
"mammoth",
"qrcode",
Expand Down
50 changes: 33 additions & 17 deletions public/offline.html
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@
.crisis-tel {
display: inline-flex;
align-items: center;
min-height: 2.75rem;
min-height: 3rem;
margin-top: 0.35rem;
color: var(--page-accent);
font-size: 1.15rem;
Expand Down Expand Up @@ -370,25 +370,12 @@
<!-- brand-mark:end -->
<h1 id="offline-title">PsychSift is offline</h1>
<p>
Reconnect to search, generate answers, or open private clinical documents and account data. This offline page
can still show the public crisis numbers and any Mental Health Act deadlines your clinician has signed off, as
quotes below.
</p>
<section class="on-call" id="on-call-offline" aria-labelledby="on-call-offline-title" hidden>
<h2 id="on-call-offline-title">On Call, saved on this phone</h2>
<p class="warning" id="on-call-offline-saved"></p>
<div id="on-call-offline-lists"></div>
</section>
<p class="privacy-note">
For privacy, the PWA cache stores only this offline screen and public application assets. It does not store or
replay clinical queries, answers, documents, uploads, signed URLs, or API responses. The numbers and quotes
below are public, non-patient information, not stored or replayed patient data, and each shows its source and
the date it was checked. If you use On Call, this screen can also show the shared On Call numbers your browser
already saved in the last seven days, never your personal entries; they are cleared when you sign out.
Reconnect to search, get answers, or open private clinical documents and account data. The numbers below work
without a connection.
</p>
<!-- offline-pack:start (generated by scripts/build-offline-pack.ts) -->
<section aria-labelledby="offline-crisis-heading" class="offline-pack-section">
<h2 id="offline-crisis-heading">Crisis support numbers</h2>
<h2 id="offline-crisis-heading">Urgent numbers</h2>
<ul class="crisis-list">
<li class="crisis-item">
<p class="crisis-name">Emergency services</p>
Expand All @@ -399,6 +386,23 @@ <h2 id="offline-crisis-heading">Crisis support numbers</h2>
<a href="https://www.triplezero.gov.au/" target="_blank" rel="noopener noreferrer">Source</a>
</p>
</li>
<li class="crisis-item">
<p class="crisis-name">Poisons Information Centre</p>
<a class="crisis-tel" href="tel:131126">13 11 26</a>
<p class="crisis-availability">24 hours, every day</p>
<p class="crisis-caveat">
For overdose or poisoning advice. Call 000 if the person has collapsed or is not breathing.
</p>
<p class="crisis-checked">
Checked 13 October 2026 &middot;
<a
href="https://scgh.health.wa.gov.au/Our-Services/Service-directory/Poisons"
target="_blank"
rel="noopener noreferrer"
>Source</a
>
</p>
</li>
<li class="crisis-item">
<p class="crisis-name">Mental Health Emergency Response Line (MHERL) — Perth metropolitan</p>
<a class="crisis-tel" href="tel:1300555788">1300 555 788</a>
Expand Down Expand Up @@ -482,6 +486,11 @@ <h2 id="offline-crisis-heading">Crisis support numbers</h2>
</li>
</ul>
</section>
<section class="on-call" id="on-call-offline" aria-labelledby="on-call-offline-title" hidden>
<h2 id="on-call-offline-title">On Call, saved on this phone</h2>
<p class="warning" id="on-call-offline-saved"></p>
<div id="on-call-offline-lists"></div>
</section>
<section aria-labelledby="offline-act-heading" class="offline-pack-section">
<h2 id="offline-act-heading">Mental Health Act deadlines</h2>
<p class="pack-caution">Reference only — check against the Act and your service's procedure.</p>
Expand Down Expand Up @@ -605,6 +614,13 @@ <h2 id="offline-act-heading">Mental Health Act deadlines</h2>
</ul>
</section>
<!-- offline-pack:end -->
<p class="privacy-note">
For privacy, this page stores only this offline screen and public application assets. It does not store or
replay clinical queries, answers, documents, uploads, signed URLs, or API responses. The numbers and quotes
above are public, non-patient information, each with its source and the date it was checked. If you use On Call,
this screen can also show the shared On Call numbers saved on this phone in the last seven days, never your
personal entries; they are cleared when you sign out.
</p>
<a class="retry" href="/">Try again</a>
</main>
<script>
Expand Down
2 changes: 1 addition & 1 deletion public/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

const CACHE_PREFIX = "clinical-kb-pwa-";
const CACHE_VERSION = "2026-10-11-v1";
const CACHE_VERSION = "2026-10-13-v1";
const SHELL_CACHE = `${CACHE_PREFIX}shell-${CACHE_VERSION}`;
const STATIC_CACHE = `${CACHE_PREFIX}static-${CACHE_VERSION}`;
const STATIC_CACHE_PREFIX = `${CACHE_PREFIX}static-`;
Expand Down
25 changes: 22 additions & 3 deletions scripts/build-offline-pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { resolve } from "node:path";

import { format, resolveConfig } from "prettier";

import { WA_CRISIS_CONTACTS, type PublicCrisisContact } from "@/lib/crisis-contacts";
import { POISONS_INFORMATION_CONTACT, WA_CRISIS_CONTACTS, type PublicCrisisContact } from "@/lib/crisis-contacts";
import { isReviewedTimeframe, type MhaTimeframeEntry } from "@/lib/mha-timeline";
import { isDirectEntrypoint } from "./lib/is-entrypoint.mjs";

Expand Down Expand Up @@ -189,7 +189,7 @@ function renderCrisisContact(contact: PublicCrisisContact): string {
function renderCrisisSection(contacts: readonly PublicCrisisContact[]): string {
const items = contacts.map(renderCrisisContact).join("\n");
return ` <section aria-labelledby="offline-crisis-heading" class="offline-pack-section">
<h2 id="offline-crisis-heading">Crisis support numbers</h2>
<h2 id="offline-crisis-heading">Urgent numbers</h2>
<ul class="crisis-list">
${items}
</ul>
Expand Down Expand Up @@ -241,9 +241,28 @@ ${items}
*/
export function buildOfflinePackMarkup(timeframesPath: string): string {
const reviewed = loadReviewedTimeframes(timeframesPath);
return `${renderCrisisSection(WA_CRISIS_CONTACTS)}${renderActSection(reviewed)}`;
return `${renderCrisisSection(offlineCrisisContacts())}\n${ON_CALL_SAVED_SECTION}${renderActSection(reviewed)}`;
}

/**
* The offline page's numbers, urgent first (design audit D06, 13 Oct 2026): 000, then the
* Poisons Information Centre, then the rest of the shared crisis list in its own order.
*/
export function offlineCrisisContacts(): PublicCrisisContact[] {
const [emergency, ...rest] = WA_CRISIS_CONTACTS;
return [emergency, POISONS_INFORMATION_CONTACT, ...rest];
}

/**
* The On Call numbers this phone already saved, filled in by the page's own script. Sits between
* the crisis numbers and the Act quotes, so the numbers a doctor rings come before anything to read.
*/
const ON_CALL_SAVED_SECTION = ` <section class="on-call" id="on-call-offline" aria-labelledby="on-call-offline-title" hidden>
<h2 id="on-call-offline-title">On Call, saved on this phone</h2>
<p class="warning" id="on-call-offline-saved"></p>
<div id="on-call-offline-lists"></div>
</section>`;

/**
* Replaces the region between the offline-pack sentinels in `currentHtml` with
* `markup`, leaving the rest of the document untouched. Throws if the
Expand Down
2 changes: 1 addition & 1 deletion src/app/(search-app)/forms/clock/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { consolidatedModeSearchPath } from "@/lib/consolidated-mode-home-redirec
import { mhaClockForms } from "@/lib/psychiatry-hub/mha-clock-forms";

export const metadata: Metadata = {
title: "MHA clock | PsychSift",
title: "MHA time limits | PsychSift",
description: "Every Mental Health Act form you are holding, with how long it has been running and its time limits.",
};

Expand Down
2 changes: 1 addition & 1 deletion src/app/(search-app)/psychiatry/mha-clock/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MhaClockPage } from "@/components/psychiatry/mha-clock-page";
import { mhaClockForms } from "@/lib/psychiatry-hub/mha-clock-forms";

export const metadata: Metadata = {
title: "MHA clock | PsychSift",
title: "MHA time limits | PsychSift",
description: "Every Mental Health Act form you are holding, with how long it has been running and its time limits.",
};

Expand Down
4 changes: 4 additions & 0 deletions src/app/api/answer/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { queryClassForClinicalMode, queryForClinicalMode } from "@/lib/clinical-
import { resolveSearchScope } from "@/lib/search-scope";
import { resolveRetrievalAccessScope } from "@/lib/owner-scope";
import { parseJsonBody } from "@/lib/validation/body";
import { rejectPatientDetailQuestion } from "@/lib/clinical-question-patient-check-server";
import {
buildGovernedAnswerClientResponse,
buildGovernedDemoAnswerClientResponse,
Expand Down Expand Up @@ -69,6 +70,9 @@ export async function POST(request: Request) {
let body: AnswerRequestBody | null = null;
try {
const answerBody = await parseJsonBody(request, answerRequestSchema, "Invalid answer request.");
// Refuse a question that looks like it holds a patient detail before it reaches the
// AI service (audit S2, 2026-10-13). The page checks first; this is the backstop.
rejectPatientDetailQuestion(answerBody);
if (answerBody.summaryMode) {
return jsonError(
new PublicApiError("Document summaries require the streaming answer endpoint.", 400, {
Expand Down
4 changes: 4 additions & 0 deletions src/app/api/answer/stream/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { safeErrorLogDetails } from "@/lib/privacy";
import { buildServerTimingHeader, preambleServerTimingEntries } from "@/lib/server-timing";
import { startSseHeartbeat } from "@/lib/sse-heartbeat";
import { parseJsonBody } from "@/lib/validation/body";
import { rejectPatientDetailQuestion } from "@/lib/clinical-question-patient-check-server";
import { answerRequestSchema, type AnswerRequestBody } from "@/lib/validation/answer-request";
import type { AnswerStreamEventMap, AnswerStreamEventName } from "@/lib/answer-stream-contract";
import { toPublicAnswerProgressEvent } from "@/lib/answer-progress-public";
Expand Down Expand Up @@ -309,6 +310,9 @@ function streamAnswer(
export async function POST(request: Request) {
try {
const body = await parseJsonBody(request, answerRequestSchema, "Invalid answer request.");
// Refuse a question that looks like it holds a patient detail before it reaches the
// AI service (audit S2, 2026-10-13). The page checks first; this is the backstop.
rejectPatientDetailQuestion(body);
const streamAbortController = new AbortController();
const streamSignal = mergeAbortSignals([request.signal, streamAbortController.signal]);
if (isDemoMode()) return streamAnswer(body, resolveRetrievalAccessScope(), streamSignal, streamAbortController);
Expand Down
Loading
Loading