# PRD — DoctorX_Josh

Evidence date: 2026-10-16.

Source: `git@bitbucket.org:overdrive/doctorx_josh.git`, `master` at `01973b1` (2021-05-04).

Provenance labels used below: **Documented**, **Implemented**, **Inferred**, **Planned**, **Unknown**.

No requirements document exists in the source tree. Requirements below are recovered from `README.md`, route config, and the screens. Where the code and a reasonable reading of the labels disagree, both are recorded.

## Product purpose

**Documented.** `README.md`: "Doctor X is a demo xray evaluation application" that uses Amplify and Lambda. The page title is "Doctor X". The web app manifest name is "Doctor X by Josh".

**Inferred.** A doctor signs in, submits an X-ray diagnosis request, and maintains a profile. A super admin maintains a doctor directory.

**Planned.** `README.md` TODO: "just about everything". No other backlog, ticket, or roadmap file is in git.

**Unknown / Requires Confirmation.** Intended clinical users, whether a real model evaluates X-rays, data-retention obligations, and whether the demo was ever put in front of a customer.

## Users and roles

| Actor | Evidence | What they can open |
|---|---|---|
| Anonymous visitor | Public routes in `src/routes/PublicRoutes.js` | Landing, login, register, forgot password |
| Signed-in user | `localStorage.roles` is set. `src/utils/index.js` `isLoggedIn` | `/app/home`, `/app/diagnosis`, `/app/profile` |
| Super admin | Route `permission: [Roles.SUPER_ADMIN]` in `src/config/PrivateRoutesConfig.js` | Those three, plus `/app/users` |
| Doctor | `Roles.DOCTOR` is `'DOCTOR'` in `src/config/Roles.js` | No route requires `DOCTOR`. The role exists so the users and profile dropdowns can offer it |

`Roles.js` comments that these are placeholder roles. Registration of a brand-new doctor row sends `strRole: "['GUEST']"` (`src/components/Register.js`). `GUEST` is not a key in `Roles.js`.

The app does not read Cognito groups. The role used for navigation is whatever `/josh/getuuid/` returns in `strRole`, stored as a raw string and later `JSON.parse`d.

## How a session works

**Implemented** in `src/routes/Auth.js`, `src/routes/PrivateRoutes.js`, and `src/utils/index.js`.

1. `isLoggedIn()` is true when `localStorage` has a `roles` item. The comment in `isLoggedIn` calls this a temporary check.
2. A visitor who already has `roles` and hits a public route is redirected to `/app/home`.
3. A visitor who hits `/app` without `roles` is redirected to `/`.
4. Allowed private routes are the config entries whose `permission` is missing, empty, or intersects the parsed `roles` array (`lodash` `intersection`).
5. Logout calls `Auth.signOut()` and removes `roles` and `userDetails`.

Amplify is also configured with `mandatorySignIn: true` (`src/index.js`). That Cognito session is separate from the `localStorage` flag. `unityRequests.js` has `Auth.currentSession()` commented out. API calls therefore depend on Amplify's own current user, while the router depends on `localStorage`.

`userDetails` stored at login is `{ doctorID, name, surname, userGroup }`. `userGroup` is `response.pool.userPoolId` from the Cognito sign-in result.

## Major workflows

### Land and sign in

1. `/` renders `LandingPage`: top nav "Doctor X", links Login and Register, and a credit link to a YouTube URL.
2. `/login` requires a non-blank email and password (`Login.js`).
3. `Auth.signIn(email, password)`.
4. If `attributes.email_verified` is not `true`, a modal offers to go to `/Register?verify=<email>`. The path uses a capital R. The route is `/register`.
5. If verified, `API.post("unity", "/josh/getuuid/", { strEmail })`. The body is `JSON.parse`d and index `[0]` is kept.
6. `localStorage.roles` = `body.strRole` (not passed through `JSON.stringify`).
7. `localStorage.userDetails` = doctor id, name, surname, and the pool id.
8. `POST /josh/update/` with `DoctorID`, `strEmail`, and `dtLastLoggedIn` formatted as UTC `YYYY-MM-DD HH:mm:ss` (the `T` and milliseconds/`Z` stripped from `toISOString()`).
9. After 100 ms, `history.push('/app/home')`.

`NotAuthorizedException` alerts "Incorrect email or password". Any other error alerts `error.message`.

A `debugger` statement sits between sign-in and the verified check.

### Register

`isMultifactor` is hard-coded `true`, so the code step always runs. The comment says to set that flag when Cognito MFA is enabled. The code path is `Auth.confirmSignUp`, which is email confirmation, not TOTP MFA.

1. Required fields: email, name, surname, password, confirm password (must match), and the conditions checkbox.
2. Checkbox label opens two modals. Terms text: the user shall not misuse the site and "Josh is the coolest", plus a pointer to the landing-page credit link. Privacy text: submitted data is stored and maintained as far as required to maintain the service.
3. `Auth.signUp` with username = email, plus an email attribute. Name and surname are not sent to Cognito.
4. `UsernameExistsException` is shown on the email field. Other errors alert `error.message`.
5. Confirmation: non-blank code, `Auth.confirmSignUp`, then `Auth.signIn` with the same password.
6. If the email is verified, `POST /josh/getuuid/` with `strEmail`.
7. If the parsed body is `undefined` or `[]`, `POST /josh/create/` with `DoctorID` = Cognito username, name, surname, email, `strRole` = the string `['GUEST']` (single quotes), `refUserID` = Cognito username, `strUserCreatedName` = name + surname.
8. Otherwise `POST /josh/update` (no trailing slash) with `DoctorID` = Cognito username, `strEmail`, and `refUserID` = `response.UserID` from `confirmSignUp`.
9. Success screen links to `/login`.
10. "Resend" calls `Auth.resendSignUp` and alerts "New invite sent!" or "Error resending invite".

A query `?verify=<email>` on load pre-fills the email and skips to the code step. The condition is `emailVerification[0] === "verify" && emailVerification[0].length > 0`. The second clause is true whenever the first is true.

### Reset password

1. Email and confirm-email must be non-blank and equal.
2. `Auth.forgotPassword(email)`.
3. Code, new password, and confirm password must be non-blank, and the two passwords must match. The code field is `type="tel"`. Validation errors for this step are computed and then not passed to the controls (`ForgotPassword.js` confirmation form has no `isInvalid`).
4. `Auth.forgotPasswordSubmit(email, code, password)`.
5. Success screen links to `/login`.

### Home

`/app/home` shows "Welcome back Dr. {surname}! What do you have for us today?" using `getUserDetails().surname`. No further action.

### Diagnosis

`/app/diagnosis`.

1. Type, model, and xray are required to be non-blank. The messages say "Must Select an option!" and "Must Select a file!".
2. The type `<select>` has one option, value `"Type"`. The model `<select>` has one option, value `"Model"`. Those values are non-blank, so they pass the blank check.
3. The file input's `onChange` stores `e.target.value` (the browser's display string) in `xray`. There is no `Storage.put` and `MAX_ATTACHMENT_SIZE` is never read.
4. Submit `POST /josh/diagnose` with `{ type, model, xray, diagnosis }`.
5. The handler `JSON.parse`s `body`, takes `[0].result`, and `setField("Diagnosis", ...)`. The form state key and the textarea value are `"diagnosis"`. The capitalised key does not update the textarea.
6. Failure alerts "An API error occoured".
7. Reset clears type, model, xray, and diagnosis.

### Profile

`/app/profile` loads with `POST /josh/get/` and body `{ id: doctorID }` from `localStorage`.

The form shows email, first name, surname, a disabled role dropdown (`SUPER_ADMIN` and `DOCTOR`), and city. Save requires name, surname, email, and city to be non-blank. `POST /josh/update/` sends `DoctorID`, `strName`, `strSurname`, `strEmail`, `strCity`. It does not send the role. A `debugger` statement runs before the request.

Load maps `body.strCity` into the city field. Role display tries `JSON.parse(body.strRole)` and, on failure, wraps the raw value in a one-element array.

### Doctor directory

`/app/users` is omitted from the nav unless the parsed roles intersect `['SUPER_ADMIN']`.

On load, `POST /josh/getusers/` and `JSON.parse(body)` becomes the table. Columns: name, surname, email, city (`strCity`), role (`strRole`), and an EDIT button. Headers sort. Each column has a starts-with text filter. A global search box filters the table. An "Add Doctor" button opens the same dialog as EDIT with empty fields.

Dialog required fields: name, surname, email, city, and at least one role. Role options are `Object.keys(Roles)` (`SUPER_ADMIN`, `DOCTOR`).

Save for a new row:

1. Builds a payload with `DoctorID`, names, email, `strCity`, `strRole` (the selected array), `userPoolID` (the signed-in user's pool id), and `strUserCreatedName`.
2. Calls `Auth.adminCreateUser` with `UserPoolId`, `username` = email, and attributes `email` and `email_verified: true`.
3. A block above that call builds `MessageAction: "SUPRESS"` when `getField('sendmail') === false`. The checkbox state key is `sendMail`. The attributes object that includes `MessageAction` is not the object passed to `adminCreateUser`.
4. Deletes `DoctorID` and `POST /josh/create/`. Errors are logged, not alerted, inside the inner try. The outer catch alerts "An API error occoured".

Save for an existing row deletes `strUserCreatedName` and `POST /josh/update/`.

EDIT loads `strLocation` into the city field. The table column and the save payload use `strCity`.

"Resend Invitation" validates the dialog, then `Auth.resendSignUp(email)`. It is shown for both new and existing rows.

The dialog does not close itself after a successful save, and the table is not reloaded.

## Functional requirements

### FR-001 — Public landing

The anonymous user sees a landing page branded "Doctor X" with links to login and register.

- Trigger: GET `/` while `localStorage.roles` is absent.
- Output: `LandingPage` and `TopNav`.
- Evidence: `src/components/LandingPage.js`, `src/routes/PublicRoutes.js`.
- Implementation: Implemented.
- Confidence: High.

### FR-002 — Login

The user submits email and password. Blank values are rejected in the client. A verified Cognito user is sent to `/app/home` after the doctor lookup and last-login update. An unverified user is offered the confirmation screen. A bad password shows "Incorrect email or password".

- Evidence: `src/components/Login.js`.
- Implementation: Implemented.
- Confidence: High.
- Note: the unverified redirect path is `/Register`, while the route is `/register`.

### FR-003 — Route gate

`/app/*` requires `localStorage.roles`. Public routes redirect to `/app/home` when that item exists. Users is included only for `SUPER_ADMIN`.

- Evidence: `src/routes/Auth.js`, `src/routes/PrivateRoutes.js`, `src/utils/index.js`, `src/config/PrivateRoutesConfig.js`.
- Implementation: Implemented.
- Confidence: High.
- Constraint: `JSON.parse` of `roles` must succeed. The registration create payload writes a single-quoted string that is not JSON.

### FR-004 — Self registration

The user must supply email, name, surname, matching passwords, and accept the in-app terms and privacy text. Cognito sign-up uses the email as the username. A confirmation code is then required. A missing doctor row is created; an existing row is updated.

- Evidence: `src/components/Register.js`.
- Implementation: Implemented.
- Confidence: High.
- Unknown: Cognito password policy. This client only checks non-blank and match.

### FR-005 — Password reset

The user confirms the email twice, receives a code, and sets a matching new password.

- Evidence: `src/components/ForgotPassword.js`.
- Implementation: Implemented.
- Confidence: High.

### FR-006 — Home greeting

The signed-in user sees a greeting that includes the surname stored at login.

- Evidence: `src/components/Home.js`.
- Implementation: Implemented.
- Confidence: High.

### FR-007 — Diagnosis request

The signed-in user submits type, model, and an X-ray file value. The client posts them to `/josh/diagnose` and expects `JSON.parse(body)[0].result`.

- Evidence: `src/components/Diagnosis.js`.
- Implementation: Implemented as a form and a POST. Selectable type and model values, file upload, and writing the result into the visible textarea were not found.
- Confidence: High on what the code does. **Unknown** what the API is supposed to return beyond the property the client reads.

### FR-008 — Own profile

The signed-in user loads and updates name, surname, email, and city for the doctor id stored at login. The role control is disabled and is not sent on update.

- Evidence: `src/components/Profile.js`.
- Implementation: Implemented.
- Confidence: High.

### FR-009 — Doctor directory

A super admin lists doctors, filters and sorts the table, and opens an add or edit dialog. Save creates a Cognito user and a doctor row, or updates the row.

- Evidence: `src/components/Users.js`, `src/config/PrivateRoutesConfig.js`.
- Implementation: Implemented.
- Confidence: High on the calls. Whether `Auth.adminCreateUser` can succeed from this browser bundle is **Unknown / Requires Confirmation**.

### FR-010 — Logout

Logout signs out of Cognito, clears `roles` and `userDetails`, and goes to `/`.

- Evidence: `src/components/common/TopNav.js`.
- Implementation: Implemented.
- Confidence: High.

### FR-011 — Unknown private URL

A signed-in user who opens an `/app` path that is not an allowed route sees "404 not found" and a link to `/`.

- Evidence: `src/routes/MapAllowedRoutes.js`, `src/components/common/NotFound.js`.
- Implementation: Implemented.
- Confidence: High.

## Integrations

| System | What this repo configures or sends | Direction | Auth as configured | Failure behaviour in the client |
|---|---|---|---|---|
| Amazon Cognito user pool `eu-west-1_qAilrD6X0`, client `66ajgtvc57ig3rh5hivfmat2a` | Sign-up, confirm, sign-in, forgot password, sign-out, resend, `adminCreateUser` | Browser → Cognito | Amplify Auth. `mandatorySignIn: true` | Alerts or field errors as in the workflows above |
| Cognito identity pool `eu-west-1:f4892151-eb6c-4e57-8b44-971cf1b1ad65` | Passed to Amplify Auth and Storage | Config only | Identity pool id in `src/aws-config.js` | No call site found |
| API Gateway `https://nhyfbveswk.execute-api.eu-west-1.amazonaws.com/prod`, Amplify name `unity` | Doctor and diagnosis POSTs | Browser → API | Amplify attaches the Cognito session. The helper does not set a custom authorizer | `console.log` plus an alert on the screens that catch |
| Amazon S3 bucket `unity-lite-files` | Amplify Storage config. `MAX_ATTACHMENT_SIZE` 5000000 | Config only | Identity pool id | No `Storage` method call under `src/` |

Region for all three is `eu-west-1`.

The previous API host, replaced in `01973b1`, was `https://2dg2tax5pg.execute-api.eu-west-1.amazonaws.com/prod`.

`DoctorX_lambda_api_Josh` (Silicon Ark, lambda tip `40da799`) documents POST paths `josh/create`, `josh/getusers`, `josh/update`, `josh/get`, `josh/getuuid`, and `josh/diagnose`, with an IAM authorizer. This UI calls those path strings. Pairing the two repositories is **Inferred**. The gateway id is not in the lambda repository.

## Constraints

- **Documented.** Start with `yarn install` then `yarn start`.
- **Implemented.** Client-side required-field checks listed in `business-rules.md`. No shared validator.
- **Implemented.** Navigation role check is `localStorage`, not a server session.
- **Implemented.** Service worker is shipped and then `unregister()` is called (`src/index.js`). Offline use is not enabled.
- **Unknown.** Hosting, TLS, custom domain, supported browsers beyond the `browserslist` in `package.json`, and retention of doctor or X-ray data.

## Removed behaviour

`77de4c8` added `lambda/addcalendarevent.js`, `getcalendarevents.js`, `removecalendarevent.js`, and `updatecalendarevent.js`. `b0527bc` deleted them. No calendar screen remains. FullCalendar packages are still declared and unused.

## What requires confirmation

- Product owner, live status, and whether the README TODO is still the plan.
- That API `nhyfbveswk` is `josh-lambda-api`, and that trailing-slash differences (`/josh/update` vs `/josh/update/`, `/josh/diagnose` vs a path without a slash) are accepted by API Gateway.
- Cognito password policy, MFA, and whether browser `adminCreateUser` is expected to work.
- The shape of `strRole` stored in the database (JSON array string vs the single-quoted `['GUEST']` this client sends).
- Whether `confirmSignUp` returns an object with `UserID`.
- Whether diagnosis is supposed to upload a file and display `result`.
- Shared use of bucket `unity-lite-files` and the Cognito ids with Unity Lite.
