# PRD — DigitalBio

Evidence date: 2026-10-17. Source: `https://bitbucket.org/overdrive/digitalbio.git`, `master` `2739231` (2023-03-07, "Name changes and changed tile to show on ML Results page").

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

No formal PRD existed in the repository or in `silicon_ark/DigitalBio/` before this file. Requirements below are recovered from the React screens, the Amplify auth and API config, and the Lambda in `amplify/backend/function/SearchTiles/src/app.js`. They are not a new product decision.

AWS account numbers, role ARNs, and secret values are omitted. The API host below is the one written in `README.md`. It was not called during this investigation.

## Product purpose

A person opens a web app. The document title is **Digical Digital**. The package and Amplify project are **digitalbio**. Until Cognito accepts them, the shell does not render. After sign-in they see their username, a Sign out button, and three navigation buttons: **ML Results**, **Jobs**, and **Canvasses**.

**Implemented** in `public/index.html`, `src/App.js`, and `src/Components/AppHeader.jsx`.

The README does not describe the product. It lists four local steps and one API host. Provenance: **Documented**.

The screens are a read-only lookup of slide tiles, per-tile result rows, job rows, and canvases. The clinical or scientific meaning of those words is **Unknown / Requires Confirmation**. The sidebar label "ML Results" and the `bbox` columns `results` and `score` are the only evidence that some rows are model output. The model is not in this repository.

## Users and roles

One kind of person is represented: a Cognito user who can complete Amplify's authenticator.

| Capability | What the code does | Provenance |
|---|---|---|
| Sign in | `withAuthenticator` wraps `App`. The custom `SignIn.jsx` is never imported | **Implemented** |
| Sign out | Header button calls the `signOut` argument from `withAuthenticator` | **Implemented** |
| Username on screen | `user.username` | **Implemented** |
| Groups or roles | `userPoolGroupList` is `[]`. No screen checks a group | **Implemented** |
| Self-service sign-up | The authenticator options do not set `hideSignUp`. A separate `SignUp.jsx` exists and is never imported | **Inferred** that the Amplify widget still offers sign-up. Not executed in a browser |
| Unauthenticated browsing | Identity pool `allowUnauthenticatedIdentities` is false. The shell is behind the authenticator | **Implemented** |

There is no admin screen, no invite flow, and no role matrix. Who is allowed to have a Cognito user is **Unknown / Requires Confirmation**.

Checked-in pool settings (`amplify/backend/auth/digitalbio5fd52944/cli-inputs.json`):

- User pool and identity pool. Sign-in name is the Cognito username (`usernameAttributes` and `aliasAttributes` are empty). Email is required and is the verification channel. Usernames are not case-sensitive.
- MFA is off. SMS is listed as an MFA type and is not enabled.
- Password minimum length 8. `passwordPolicyCharacters` is empty and `defaultPasswordPolicy` is false.
- Refresh token validity 30. The app client does not generate a secret.

`src/Components/SignIn.jsx` labels its field "Email" and calls `Auth.signIn`. Nothing imports it. The live gate is the Amplify widget. The widget's field label is **Unknown** because `aws-exports.js` is not in the clone.

## Major workflows

### 1. Open the app and sign in

Trigger: browser loads the CRA bundle (`src/index.js`).

Inputs: Cognito username and password, collected by `withAuthenticator`. This repo does not set the form fields.

Behaviour:

- `Amplify.configure(awsconfig)` runs with `./aws-exports`. That file is gitignored and is not in this clone.
- `App` is exported only through `withAuthenticator`, with `Hearder` passed as the authenticator `Header` (the logo).
- After a session, the grid renders: header row, navigation column, content column. `pageName` starts as `Dashboard`.
- The navigation button for that page reads **ML Results**. The page-state string stays `Dashboard`.

Output: the ML Results screen, which immediately requests `/getlastslides`.

Error / empty: no error boundary. `public/index.html` noscript text is the CRA default: "You need to enable JavaScript to run this app." A missing `aws-exports.js` fails the bundle at import. Authenticator errors are Amplify's own UI. They are not customised in this repo.

### 2. Look up tiles on ML Results

Trigger: the ML Results button, or the initial page.

Actor: signed-in user.

Behaviour (`src/Pages/Dashboard.jsx`):

- On mount, `GET /getlastslides`. The server returns up to 10 `image` rows ordered by `slide`, not by date or id.
- The table header reads "Tiles Found" when the array is non-empty, a loader while `isLoading` is true and the array is empty, and "No results found" otherwise.
- Each row shows `result.name` and is a click target. Clicking calls `selectTile`, which stores the row and sets the page to `Result Tile Viewer`.
- The search placeholder is "Slide No. Search".
- While the typed value is 1 or 2 characters, the table is cleared and no request is sent.
- At 3 or more characters, `GET /searchslides/{value}`. The server returns up to 10 `image` rows whose `slide` contains that value, case-insensitive, ordered by `slide`.
- Clearing the field, or deleting back to an empty field, calls `/getlastslides` again.
- Submitting the search field (the search button or Enter) sets the page to `Slide Explorer`. It does not wait for a minimum length.

The search matches `slide` and the table shows `name`. Commit `2739231` changed the cell from `result.slide` to `result.name`.

Error: the promise `catch` logs to the console and does not clear `isLoading`. A failed request can leave the loader or the placeholder rows on screen. **Implemented**.

### 3. Browse tiles in Slide Explorer

Trigger: submit on a search field whose placeholder is not "Job No. Search" and not "Canvas No. Search" (`src/Components/AppSearchField.jsx`).

The ML Results placeholder is "Slide No. Search", so its submit opens this page. The Canvasses placeholder is also "Slide No. Search", so its submit opens this page too. Slide Explorer then reads `searchValue` from `App`, and only ML Results writes that value. A submit from Canvasses therefore searches with the ML Results text, which may be empty. **Implemented** call path. **Inferred** consequence. Not executed in a browser.

Behaviour (`src/Pages/SlideExplorer.jsx`):

- Shows "Search Number :" and the search text.
- `GET /getslides/{searchValue}` and presigns each row that has `uri`.
- The server returns every `image` row whose `slide` contains the value. There is no `LIMIT`.
- Each card is 200 by 200, uses the presigned URL as a background, and shows `slide.name` on the card. Clicking calls `selectTile` and opens the tile viewer.
- If `signedUrl` is missing, the card uses `src/Images/No_Image_Available.jpg`.
- While the array is empty and loading, four placeholders show. After loading, an empty array shows "No Results found".

There is no navigation button for this page and no button back to ML Results. The sidebar is still available. Commit `d442a33` removed a Dashboard button from this page.

### 4. View one tile and its results

Trigger: click a tile row or a Slide Explorer card.

Behaviour (`src/Pages/ResultTileViewer.jsx`):

- `GET /maximageid` stores `res[0].max`.
- The selected row's `id` becomes `slideId`.
- `GET /getslidebyid/{slideId}` plus a presign. The heading "Slide Number" shows `slide.slide`. The heading "Tile Number" shows `slide.name`.
- If that id returns no row and the direction is forward, the id increments while it is below the max. If the direction is backward, the id decrements while it is not 1. Previous/next buttons set that direction.
- Previous is disabled when `slideId` is 0 or 1. Next is disabled when `slideId` equals the max id.
- The image sits in `react-zoom-pan-pinch` with zoom in, zoom out, and reset. If there is no presigned URL and the row has neither `uri` nor `name`, a placeholder shows. If there is no presigned URL but `uri` or `name` is set, the no-image asset shows.
- `GET /imageresult/{id}` fills a table of `bbox` rows: `id`, `results`, and `Math.round(score * 100)` with a percent sign. The column heading is "Confidence".
- While `slideResults` is empty, five placeholder rows show. That includes a tile that truly has no bbox rows. The page does not distinguish "still loading" from "no results".

`setPageName` is accepted and never called. The way back is the sidebar.

Error: failures are `console.log` only.

### 5. Look up jobs

Trigger: the Jobs button.

Behaviour (`src/Pages/Jobs.jsx`):

- On mount, `GET /getlastjobs`. The server returns up to 10 `batch_transform_jobs` rows ordered by `name`, not by `created_date`.
- Columns: ID (`id`), Image Name (`name`), Status, Started (`created_date`). A Job Title cell that would have shown `title` is commented out.
- Status is the string `Done` when `perc === 1`. Otherwise it is `Math.round(perc * 100)` with a percent sign.
- Started is formatted as `YYYY/MM/DD H:M` from the browser's local reading of `new Date(created_date)`. Hours and minutes are not padded.
- The placeholder is "Job No. Search". The search button is hidden and submit does nothing, because the placeholder equals "Job No. Search".
- Any non-empty change calls `GET /getjobbyname/{value}`. The server returns every job whose `name` contains the value, case-insensitive, ordered by `name`. There is no `LIMIT` and no minimum length.
- Empty input or clear reloads `/getlastjobs`.
- Rows use a pointer cursor. There is no click handler and no job detail page.
- Empty and not loading: "No Jobs Found". Loading with an empty array: a loader. Loading with a stale array: placeholder rows.

Error: `catch` logs and does not clear `isLoading`.

### 6. Look up canvasses

Trigger: the Canvasses button. Page state becomes `Canvas Search`.

Behaviour (`src/Pages/CanvasSearch.jsx`):

- On mount, `GET /getlastcanvas`, then presign. The server returns up to 10 `canvas` rows ordered by `name`.
- Cards are 400 by 400, background from the presigned URL or the no-image asset. The name caption is commented out, so the card has no text.
- The placeholder is "Slide No. Search". Because that string is not "Canvas No. Search", the search button is shown and submit navigates to Slide Explorer (workflow 3).
- Typing one or more characters calls `GET /getCanvas/{value}`. The server returns every `canvas` row whose `slide` contains the value, case-insensitive, ordered by `slide`. There is no `LIMIT`.
- Empty input or clear reloads `/getlastcanvas`.
- Clicking a card stores it and opens `Canvas Result Viewer`.
- Empty and not loading: "No data to display".

### 7. View one canvas and its tiles

Trigger: click a canvas card.

Behaviour (`src/Pages/CanvasResultViewer.jsx`):

- `GET /maxcanvasid`, then `GET /getcanvasbyid/{id}` with a presign.
- The heading "Canvas Number" shows `canvas.name`, not `canvas.slide`.
- A button labelled **Dashboard** sets the page back to `Dashboard` (the ML Results screen, whose sidebar label is no longer "Dashboard").
- Same zoom controls as the tile viewer, and the same previous/next id walk across `canvas.id`.
- `GET /canvastiles/{id}` lists `tiles` rows. The table shows `row/col` under "Grid Index" and `name` under "Image".
- An empty tile list shows five placeholder rows, including when the canvas has no tiles.

Error: `console.log` only.

## Functional requirements

### FR-001 — Block the shell until Cognito signs the person in

The app renders `App` only as the child of `withAuthenticator`. The logo component is the authenticator header.

- Actor: any visitor.
- Trigger: load the bundle.
- Inputs: username and password, via the Amplify widget.
- Expected: no ML Results, Jobs, or Canvasses UI before a session. After a session, the header shows `Logged In: {username}` and Sign out.
- Validation: the pool config requires an email attribute, verifies email, and sets a minimum password length of 8. No character-class rule is in the checked-in policy.
- Error: Amplify's widget. This repo adds no message of its own.
- Evidence: `src/App.js`, `amplify/backend/auth/digitalbio5fd52944/cli-inputs.json`.
- Implementation: **Implemented**. Confidence: high for the gate. The exact widget copy was not executed.

### FR-002 — Sign out

- Actor: signed-in user.
- Trigger: Sign out in the header.
- Expected: `signOut` from `withAuthenticator` runs.
- Evidence: `src/Components/AppHeader.jsx`.
- Implementation: **Implemented**.

### FR-003 — Open on ML Results

- Actor: signed-in user.
- Trigger: first render after sign-in.
- Expected: `pageName` is `Dashboard`. The sidebar button that sets that page reads "ML Results". `/getlastslides` runs.
- Evidence: `src/App.js`, `src/Pages/Dashboard.jsx`, commit `2739231`.
- Implementation: **Implemented**.

### FR-004 — Three sidebar destinations

- Actor: signed-in user.
- Trigger: ML Results, Jobs, or Canvasses.
- Expected: page state becomes `Dashboard`, `Jobs`, or `Canvas Search`. Slide Explorer, the tile viewer, and the canvas viewer are not sidebar buttons.
- Evidence: `src/App.js`.
- Implementation: **Implemented**.

### FR-005 — List tiles for a slide number

- Actor: signed-in user on ML Results.
- Trigger: type into "Slide No. Search".
- Inputs: the text. The client sends it only when length is at least 3. The server applies it as a case-insensitive contains match on `image.slide`, ordered by `slide`, limit 10.
- Expected: the table lists `name` for each returned row. The header reads "Tiles Found".
- Validation: 1 or 2 characters clear the table and do not call the API. Empty reloads the default 10.
- Error: console only. Loading flag can stay true.
- Evidence: `src/Pages/Dashboard.jsx`, `SearchTiles/src/app.js` routes `/searchslides/:searchValue` and `/getlastslides`.
- Implementation: **Implemented**.

### FR-006 — Open Slide Explorer from search submit

- Actor: signed-in user.
- Trigger: submit a search field whose placeholder is anything except "Job No. Search" and "Canvas No. Search".
- Expected: page becomes `Slide Explorer`. Cards load from `/getslides/{value}` using App's `searchValue`. Each card shows the tile name and opens the tile viewer on click.
- Output: one card per returned `image` row. No server limit.
- Empty: "No Results found".
- Evidence: `src/Components/AppSearchField.jsx`, `src/Pages/SlideExplorer.jsx`.
- Implementation: **Implemented**. The Canvasses field uses the placeholder "Slide No. Search", so it also takes this path. Whether that is intended is **Unknown / Requires Confirmation**.

### FR-007 — Show a tile image and its result rows

- Actor: signed-in user.
- Trigger: click a tile row or a Slide Explorer card.
- Inputs: the clicked row's `id`.
- Expected: slide number, tile number, a zoomable image, previous and next by numeric id, and a table of bbox `id`, `results`, and confidence.
- Validation: previous stops at id 1. Next stops at `max(image.id)`. A missing id is skipped in the current direction.
- Empty results: placeholder rows, not an explicit empty message.
- Missing image: no-image asset when the row has a `uri` or a `name` but no presigned URL.
- Evidence: `src/Pages/ResultTileViewer.jsx`, routes `/getslidebyid/:slideid`, `/imageresult/:imageID`, `/maximageid`.
- Implementation: **Implemented**. Confidence in the percent formula assumes `score` is a fraction. The stored scale is **Unknown / Requires Confirmation**.

### FR-008 — List and filter jobs

- Actor: signed-in user.
- Trigger: Jobs, then the search field.
- Inputs: any non-empty text. The server matches `batch_transform_jobs.name`, case-insensitive, no limit. The default list is 10 rows ordered by `name`.
- Expected columns: ID, Image Name, Status, Started. Status is `Done` only when `perc` is strictly equal to 1; otherwise a rounded percentage.
- The placeholder says "Job No. Search". The filter column is `name`, not `id`.
- Rows do not navigate.
- Evidence: `src/Pages/Jobs.jsx`, routes `/getlastjobs` and `/getjobbyname/:jobname`.
- Implementation: **Implemented**. Whether `name` is the job number is **Unknown / Requires Confirmation**.

### FR-009 — List and filter canvasses

- Actor: signed-in user.
- Trigger: Canvasses, then the search field.
- Inputs: one or more characters. The server matches `canvas.slide`, case-insensitive, ordered by `slide`, no limit. The default list is 10 rows ordered by `name`.
- Expected: image cards. Click opens the canvas viewer. The card caption is not rendered.
- Evidence: `src/Pages/CanvasSearch.jsx`, routes `/getlastcanvas` and `/getCanvas/:searchValue`.
- Implementation: **Implemented**.

### FR-010 — Show a canvas image and its grid

- Actor: signed-in user.
- Trigger: click a canvas card.
- Expected: "Canvas Number" bound to `canvas.name`, zoomable image, previous/next by canvas id, a Dashboard button that returns to the ML Results page, and a table of `row/col` plus tile `name`.
- Evidence: `src/Pages/CanvasResultViewer.jsx`, routes `/getcanvasbyid/:canvasid`, `/canvastiles/:canvasid`, `/maxcanvasid`.
- Implementation: **Implemented**.

### FR-011 — Presign image URIs for one hour

- Actor: the client, after a data call that returns rows with `uri`.
- Trigger: Slide Explorer, tile viewer, canvas search, canvas viewer.
- Inputs: `uri`. The helper splits on `//`, treats the next segment as the bucket, and the remainder as the key. Region is `eu-west-1`. Expiry is 3600 seconds. Credentials are `Auth.currentCredentials()`.
- Expected: `signedUrl` on the row, used as an image source.
- Error: a thrown presign rejects the whole `Promise.all`. The page's `catch` logs it.
- Evidence: `src/util/getDataWithSignedURL.js`, `src/util/s3Utils.js`.
- Implementation: **Implemented**. The URI shape stored in the database is **Unknown / Requires Confirmation**.

### FR-012 — Read the database only through SearchTiles

- Actor: the SPA.
- Trigger: every list and detail above.
- Expected: `API.get('apia19f7356', path)` and the client reads `response.data.data.rows`.
- The Lambda loads secret id `webapp_admin_user` from Secrets Manager in `eu-west-1` and connects with `pg` using `username`, `host`, `dbname`, `password`, and `port` from that secret.
- There is no POST, PUT, or DELETE route.
- Evidence: `src/util/getDataFromAPI.js`, `SearchTiles/src/app.js`.
- Implementation: **Implemented**.

## Integrations

| System | Purpose | Trigger | Direction | Auth as checked in | Failure visible to the user |
|---|---|---|---|---|---|
| Cognito user pool `digitalbio5fd52944` | Sign-in gate | Page load | Browser to Cognito | Amplify authenticator. Pool settings above | Amplify widget. Not customised here |
| Cognito identity pool | S3 credentials | Presign | Browser to STS via Amplify | Unauthenticated identities disabled | Presign throws; page logs |
| API Gateway `apia19f7356` | All data reads | Screen load or search | Browser to API | Every path in `cli-inputs.json` has `permissions.setting` `open` | Amplify reject after the Lambda fails to respond |
| Lambda `SearchTiles` | SQL | API Gateway | API to Lambda | Execution role in the template allows CloudWatch Logs only | `catch` logs and does not send an HTTP response |
| Secrets Manager | DB password | Every Lambda request | Lambda to Secrets Manager | Not granted in the checked-in policy | Logged; query then runs with no new credentials |
| PostgreSQL | System of record for the five tables | After the secret is parsed | Lambda to Postgres | Password from the secret | Logged; no HTTP response |
| S3 | Tile and canvas images | Row has `uri` | Browser to S3 | Presigned GET with the caller's Cognito credentials | Broken image or the no-image asset, depending on the screen |

The README **documents** this host: `https://z24ayjk4pg.execute-api.eu-west-1.amazonaws.com/dev`. Whether that host still routes to this Lambda is **Unknown / Requires Confirmation**.

CORS on the Express app allows any origin and any header. That is **Implemented**. It is not a documented product requirement.

## Important constraints

- Navigation is component state. There are no routes, so a result cannot be bookmarked. Refresh returns to ML Results.
- Default lists are 10 rows ordered by a name or slide column. "Last" in the function names is not an order by time, except that the Jobs screen displays `created_date`.
- Slide Explorer and the canvas and job searches do not cap the result count.
- API path parameters are concatenated into SQL text. A value that changes the statement changes the query. This is **Implemented**. It is recorded as a constraint, not as an approved business rule.
- The Lambda `catch` blocks do not send a status. The function timeout in the template is 25 seconds.
- `pg` clients are constructed per request and the code never calls `end`.
- Node runtime in the template is `nodejs14.x`.
- Frontend build commands in Amplify are `npm.cmd run-script build` and `npm.cmd run-script start` (Windows `npm.cmd`).
- `@aws-sdk/client-secrets-manager` is a frontend dependency and is not imported under `src/`. The browser does not read the database secret. The Lambda does.

## Planned behaviour

None found. No roadmap file, no issue list, and no feature flag for unfinished work. The CRA comments in `src/App.js` are not a planned to-do feature.

## Requirements that need confirmation

- Product name, and whether "Digical Digital" and `digicalBio-icon.png` are typos.
- Meaning of slide, tile, canvas, and `bbox.results`.
- Who may sign up, and whether the open API Gateway paths are intentional beside the Cognito gate.
- Score and `perc` scale.
- Job search column versus the "Job No. Search" label.
- Canvas search submit opening Slide Explorer.
- Live deployment of the `dev` stack and the README host.
