# PRD — unity-lite (UnityLite)

Evidence date: 2026-09-30. Source: `https://bitbucket.org/overdrive/unity-lite.git`, `master` `9b3a386` (2021-06-08, merge after "import graph libs").

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

No formal PRD existed in the repository or in `silicon_ark/unity-lite/` before this file. Requirements below are recovered from the React screens, the route table, the Amplify calls, and (where labelled) deleted Lambda sources that once lived under `lambda/`. They are not a new product decision.

Identifier values in `src/aws-config.js` are omitted. The login form's initial email and password literals are omitted. The files that hold them are named where the behaviour depends on their presence.

## Product purpose

A person opens a web app titled **UnityLite**. The shell brands itself **UnityLite**. While `localStorage` has no `roles` key they see a landing page, login, register, or forgot-password. After a sign-in attempt the app always navigates to `/app`. `/app` stays open only when that `roles` key is present.

**Implemented** in `public/index.html`, `src/components/common/TopNav.js`, `src/routes/Auth.js`, `src/routes/PrivateRoutes.js`, and `src/components/Login.js`.

The README calls the project "Unity - Lite", "Unity with out styling and redux components", and says it uses Amplify / lambda. Lambda handlers were present in earlier commits and were removed from this tree. Provenance of the Lambda claim: **Documented** in `README.md`; historical handlers: **Implemented** then removed (see technical documentation).

Three names are in the tree and they are not the same string: page title `UnityLite`, README title `Unity - Lite`, package name `role-based-access-control`. The web-app manifest still says `React App`. Which name is the product name is **Unknown / Requires Confirmation**.

## Users and roles

`src/config/Roles.js` defines five role strings and comments that they are "placeholder roles you can replace". Provenance of the list: **Implemented**. Provenance of "placeholder": **Documented** in that comment.

| Role | Where the route table names it | What the code actually distinguishes | Provenance |
|---|---|---|---|
| `SUPER_ADMIN` | Config, Dashboard, Profile | May open those screens when the stored roles parse to an array containing this string | **Implemented** |
| `ADMIN` | Config, Dashboard, Profile | Same gate as `SUPER_ADMIN` for those three screens | **Implemented** |
| `MANAGER` | Config, Profile. Not Dashboard | Cannot open Dashboard. Can open Config and Profile | **Implemented** |
| `CUSTOMER` | Profile only | Cannot open Config or Dashboard. Can open Profile | **Implemented** |
| `GUEST` | Defined. No route lists it | If it is the only stored role, screens with no `permission` still open (calendar, booking, task list, kanban, users, user). Config, Dashboard, and Profile do not | **Implemented** definition. No screen assigns this role |

Screens with no `permission` array are Calendar, Booking, Tasklist, Kanban, Users, and User (`src/config/PrivateRoutesConfig.js`). `getAllowedRoutes` keeps a route when `permission` is missing or empty, for any caller who already passed the `roles` key check.

`getAllowedRoutes` (`src/utils/index.js`) keeps a permissioned route when `lodash.intersection` of the route's `permission` array and the parsed `roles` value is non-empty.

Signed-in state is not a Cognito session check. `isLoggedIn()` returns true when `localStorage` has a `roles` key. The function comment says the app assumes that key means the user is authenticated, and that the check is temporary. Provenance: **Implemented**, and **Documented** in that comment.

What the API field `strRole` contains (one name, or a JSON array of names) is **Unknown / Requires Confirmation**. Login stores it with `localStorage.setItem('roles', body.strRole)` and does not `JSON.stringify` it. Route filtering then `JSON.parse`s that value. If `strRole` is not already a JSON array string, `JSON.parse` throws and the private route tree does not render. **Inferred** from those two call sites. Not executed in a browser for this document.

There is no user-administration screen that saves a role. The user form renders a multi-select of the five role strings and does not submit it.

Deleted Lambda `getuseruuid.js` (removed in `3a6ecbd`) selected from MySQL table `sysUser` where `strCognitoUuid` matched the path id. That is historical evidence that Cognito usernames were mapped to `sysUser` rows. Whether the live API still does that is **Unknown / Requires Confirmation**.

## Major workflows

### 1. Open the app

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

Behaviour:

- Amplify is configured for Auth (Cognito, `mandatorySignIn: true`), Storage (S3 bucket `unity-lite-files`), and an API endpoint named `unity`. Region in config is `eu-west-1`. Ids live in `src/aws-config.js`.
- `serviceWorker.unregister()` runs. `public/manifest.json` is linked (`display` standalone, name "Create React App Sample"). The service worker is not registered.
- `App` renders `Routes` inside a fluid Bootstrap container.
- Paths under `/app` render `PrivateRoutes`. Every other path renders `Auth`.
- `Auth` sends the visitor to `/app` when `isLoggedIn()` is true, and otherwise renders `PublicRoutes`.
- Public switch: `/forgot-password`, `/register`, `/login`, and a catch-all `LandingPage`.
- `LandingPage` shows `TopNav` with Login and Register, and the silicon overdrive credit.

Output: landing page, or a redirect into `/app` when a `roles` key already exists.

Error / empty: no splash screen. No error boundary was found. A missing `#root` is the CRA default (`public/index.html` noscript text: "You need to enable JavaScript to run this app.").

### 2. Sign in

Trigger: Login button on `/login` (`src/components/Login.js`).

Inputs: email and password state. The component initialises both from literals in the file. Those literals are omitted here.

Validation: none. The button is not disabled. The email and password `onChange` handlers do not read `e.target.value`. Each handler builds an empty array from `e.target.options` (a text input has no options) and stores that array. **Implemented**. Typing in the fields does not update the state with the typed characters. **Inferred** from those handlers. Not executed in a browser for this document.

Behaviour when the button is clicked (`handleClick`):

- `Auth.signIn(strEmail, strPassword)`.
- On a session, the access token is read (`response.signInUserSession.accessToken.jwtToken`) and is not written to `localStorage` or to an axios header. The Cognito username is `response.username`.
- `API.get("unity", "/unity/uuid/{username}")`.
- The code expects `{ body }` whose `body` is a JSON string, then takes index `0`.
- It stores `localStorage.roles` = `body.strRole` (see the parse mismatch above).
- It stores `localStorage.userDetails` = JSON `{ userID: body.UserID, userName: body.strUser }`.
- On an API exception inside the uuid call, the function returns `{ result: "Api Error" }` instead of throwing. `handleClick` then stores `roles` from `body.strRole` of that object (no such field). `setItem` coerces a missing value to the string `"undefined"`, which is truthy, so `isLoggedIn()` becomes true. `JSON.parse("undefined")` in `getAllowedRoutes` then throws. **Inferred** from `Login.js` and `utils/index.js`. Not executed here.
- `history.push('/app')` runs after the try/catch, including when `Auth.signIn` throws. A failed sign-in still navigates to `/app`. If `roles` was not set, `PrivateRoutes` redirects back to `/`.

The access token variable is unused after it is read. Session continuity for later Amplify calls depends on Amplify's own Cognito storage, which this file does not configure beyond `Amplify.configure` in `src/index.js`. **Inferred**.

A commented block in the same file offered a multi-select of the five roles and the text "Support multi roles." It is not rendered.

Outputs: `/app` navigation; `roles` and `userDetails` keys only when the statements that set them run.

### 3. Register and forgot password

Trigger: links from login, or the paths `/register` and `/forgot-password`.

Behaviour: each screen is a jumbotron whose title is "Register" or "ForgotPassword", plus a link back to `/login`. Neither calls `Auth.signUp`, `Auth.forgotPassword`, or the `unity` API.

Provenance: **Implemented** placeholders. A working registration flow is **not implemented**. The README does not describe one.

Logged-in visitors do not reach these screens: `Auth` redirects them to `/app` before `PublicRoutes` renders.

### 4. Private shell

Trigger: navigation to any path under `/app`.

Behaviour:

- If `roles` is absent, redirect to `/`.
- Otherwise `getAllowedRoutes(PrivateRoutesConfig)` filters the table.
- `TopNav` renders links for the allowed routes (prefix `/app`) and a Logout button.
- Logout removes `roles` only and `history.push('/')`. It does not remove `userDetails` and does not call `Auth.signOut()`.
- `MapAllowedRoutes` mounts each allowed component. Unknown paths under `/app` render `NotFound`.

Child routes under Config (`/staff`, `/cronjobs`) are nested in the config object. `Configer` re-filters `children` with `getAllowedRoutes` and renders links plus nested `MapAllowedRoutes` under `/app/configer`.

### 5. Calendar

Trigger: default private route `/app` / `/app/` (`path: '/'`, `exact: true`).

Actor: any signed-in visitor (no `permission` on the route).

Behaviour (**Implemented** in `Calendar.js`):

- FullCalendar month / week / day views; weekends toggle; sidebar list.
- On visible range change (`datesSet`): `POST /unity/getcalendarevents/` with `{ userID, dtStartDate, dtEndDate }` via `unityRequests`. Response `body` is JSON-parsed and mapped from DB-shaped fields (`CalendarIDitem`, `txtDescription`, `txtNotes`, `dtStartDate`, `dtEndDate`, `strFirstUser`, `refSysUserID`) to FullCalendar fields.
- Create: `prompt` for title, then `eventAdd` → `POST /unity/addcalendarevent/`. Failure calls `revert()`.
- Drag / resize: `eventChange` → `POST /unity/updatecalendarevent/`. Failure `revert()`.
- Click + confirm: `eventRemove` → `POST /unity/removecalendarevent/`. Failure `revert()`.

`INITIAL_EVENTS` from `event-utils.js` is imported and not used as the live event source.

Whether the server restricts events to that `userID` is **Unknown / Requires Confirmation**. Historical deleted `getcalendarevents.js` filtered `crmCalendar` by `refSysUserID` and date range using path parameters; the current client posts a JSON body. Live contract alignment is **Unknown / Requires Confirmation**.

### 6. Users and user edit

Trigger: `/app/users` and `/app/user`.

Users (**Implemented**):

- On mount, `POST /unity/getusers/1`. Table columns: `strUser`, `strTel`, `strJobTitle`, `strRole`, `strEmail`, `strLocation`, plus EDIT.
- EDIT links to `{ pathname: "/app/user/"+UserID, data: UserID }`.
- `makeData(20)` is computed and unused; the table uses API `users`.

User edit (**Partial / Implemented** UI only):

- Reads `location.data` as `userId` (not the path segment).
- `POST /unity/getdetails` with `{ id: userId }`, takes `[0]`.
- Form shows company fixed "Silicon Overdrive", username/email/location fields, role multi-select. Update Profile has `type="submit"` and no handler that calls an API.
- Face image: `src/assets/images/faces/placeholder.png` (present in this tree).

### 7. Dashboard, task list, placeholders

- Dashboard (`/app/dashboard`, `SUPER_ADMIN` / `ADMIN`): static Chartist cards and tables. No `unity` call.
- Task list (`/app/tasklist`): two hardcoded tasks; Add Task form does not persist; imports packages not listed in `package.json`.
- Booking, Kanban, Profile: jumbotron titles only (Kanban also links "Back to login").
- Staff / Cron Jobs: under Config, jumbotron titles only.
- Quotes, Timesheet, Pivot: present under `src/components/`; not registered in `PrivateRoutesConfig.js`. Quotes would call `/unity/getusers/1` if mounted.

## Functional requirements

| ID | Requirement | Provenance | Status |
|---|---|---|---|
| FR-001 | Present a public landing page with Login and Register links and Overdrive credit | **Implemented** | Current |
| FR-002 | Sign in with Cognito email/password then load user by Cognito username via `/unity/uuid/{username}` | **Implemented** | Current (login field handlers broken for typed input) |
| FR-003 | Treat presence of `localStorage.roles` as signed-in | **Implemented** / **Documented** in comment | Current |
| FR-004 | Filter private nav and routes by role intersection | **Implemented** | Current |
| FR-005 | Load, create, update, delete calendar events through `unity` calendar paths | **Implemented** | Current |
| FR-006 | List users from `/unity/getusers/1` | **Implemented** | Current |
| FR-007 | Open a user edit form from the list | **Partial** — load works when `location.data` is set; save not implemented | Partial |
| FR-008 | Register a new account from the UI | Route only | Not implemented |
| FR-009 | Reset password from the UI | Route only | Not implemented |
| FR-010 | Booking, Kanban, Profile, Staff, Cron Jobs business features | Titles only | Not implemented |
| FR-011 | Persistable task list | Hardcoded UI | Not implemented |
| FR-012 | Live dashboard metrics | Hardcoded charts | Not implemented |
| FR-013 | Logout clears local sign-in flag and returns to landing | **Implemented** (roles only; no Cognito signOut) | Current |
| FR-014 | Upload files to S3 via Amplify Storage | Config only | Not implemented in UI |
| FR-015 | Pivot / Plotly analysis screen | Files present, no route | Unused |

## Integrations

| System | Direction | Trigger | Data | Auth | Failure behaviour |
|---|---|---|---|---|---|
| Amazon Cognito | Client → Cognito | Login | Email, password; session JWT read then unused | Amplify Auth | Caught; still navigates to `/app` |
| API Gateway `unity` | Client → API | Login uuid; calendar CRUDs; users; getdetails | Paths under `/unity/...`; body often JSON-parsed from `body` | Amplify (Cognito session) | Calendar reverts on write failure; login returns Api Error object |
| S3 `unity-lite-files` | Configured only | None found | — | Identity pool in config | N/A |

## Constraints

- Client-only repository at tip. Backend was removed from the tree; live backend location is **Unknown**.
- Role strings are placeholders per code comment.
- CRA 3 / React 16 era stack; no pipeline or Docker in repo.
- Secrets and pool ids live in source (`aws-config.js`).

## Assumptions and unknowns

Marked **Unknown / Requires Confirmation** unless stated:

- Canonical product name.
- Whether this tip or `unity-lite-v2` is the maintained client.
- Live API Gateway / Cognito still matching `aws-config.js`.
- Exact wire format of `strRole`.
- Meaning of `1` in `/unity/getusers/1`.
- Whether deleted MySQL schema (`sysUser`, `crmCalendar`) still backs the API.
- Deployment URL and whether production traffic still hits this build.
