# Technical documentation — bi_tool_frontend_phase6

Evidence date: 2026-10-19. Source: `phase_6` at `82d7ef04`.

`DEVELOPER_DOCUMENTATION.md` in the source repo is an older architecture note. Where it conflicts, this file follows `package.json`, `src/`, and `src/setupProxy.js`.

## What it is

A Create React App 5 single-page application ("SAWIS BI", version `0.1.0`, private). The UI is React 18.2. Routing is `react-router-dom` ^7.18.2 with `BrowserRouter` and a `<base href>` basename. State is Zustand 4. Styling is Bootstrap 5.2.3, Reactstrap 9, a Tailwind 3 config, and feature CSS under `src/styles/` and `src/custom.css`. Charts are ECharts ^6.1.0. Tables use `react-data-table-component`. Icons are Font Awesome 6.

CRACO wraps start and build (`craco.config.js` only suppresses a known `@linways/table-to-excel` webpack warning). The app is not ejected.

`oidc-client` is listed in `DEVELOPER_DOCUMENTATION.md` and is not a dependency. Authentication is a custom `POST /loginAuth` plus a cookie.

## Repository identity

| Item | Value |
|---|---|
| Remote | `git@bitbucket.org:overdrive/bitool_frontend.git` |
| HTTPS | `https://bitbucket.org/overdrive/bitool_frontend.git` |
| Branch documented | `phase_6` |
| Commit | `82d7ef04f70bebd8923383e69936ac2d094eee2d` |
| Sonar project key | `overdrive_bitool_frontend_3287b814-b882-4dde-8a5d-7400f836de1f` (`sonar-project.properties`) |
| HTML title | SAWIS Report (`public/index.html`) |
| Favicon | remote `https://www.sawisonline.co.za//images/icon.ico` |

`phase_6` is not the only branch. `git ls-remote` also shows `master`, `qa`, `env/qa`, `phase3`, `phase3_new`, `phase4`, `phase5`, `phase6_pr`, and several fix branches. This document does not describe them.

History reachable from the single-branch clone: 341 commits, first `978f2c9` (2023-11-03, Jaco Brits).

## Application structure

```text
src/index.js              React root, GA4, service worker unregister
src/App.js                autoLogin effect, maps AppRoutes into <Routes>
src/AppRoutes.js          path table and guards
src/setupProxy.js         dev-only proxy (CRA)
src/api/                  domestic fetch clients
src/store/                Zustand stores
src/pages/                route screens
src/components/           navbar, charts, pivots, forms, admin, domestic, guards
src/utils/                permissions, capture model, crypto, session, drafts
src/hooks/useDomesticLookups.js
src/data/domestic/        fixtures used when a domestic GET 404s without a message
src/lib/constants.js      mapper status colours and chart label colours
public/index.html
```

Code splitting: `DomesticRoutes`, capture page, domestic reports page, and the admin subpages are `React.lazy`. `MapperLookups` is lazy inside the tools page.

## Runtime configuration

Variable names only. Values that are secrets are not copied. `.env.development.local` exists and was not transcribed.

| Name | Role |
|---|---|
| `PORT` | Dev server port. `.env.development` sets `44487`. Playwright uses the same port. |
| `HTTPS` | Present in `.env.development`. Effect is CRA's HTTPS switch. |
| `REACT_APP_API_BASE_URL` | Prefix for `fetch`. Empty in `.env.development`, so calls are same-origin and the proxy forwards them. |
| `REACT_APP_PROXY_API_TARGET` | Proxy target. Default in code `http://localhost:8080`. `.env.development` sets that default explicitly. |
| `REACT_APP_USE_AUTH_HEADERS` | When not `"false"`, logged-in report calls send `Authorization`. `.env.development` is `true`. Generated-report code checks `=== "true"` specifically (`GeneratedReportPivotTable.jsx`), which is a stricter check than `!== "false"`. |
| `REACT_APP_LOGIN_PAYLOAD_KEY` | 64-char hex AES key. Required only when the API base is not local. Not present in the `.env.development` key list read for this note. |
| `REACT_APP_RECAPTCHA_DATA_SITE_KEY` | reCAPTCHA site key. Login reads it. Not in the `.env.development` key list; dev bypasses captcha instead. |
| `REACT_APP_DISABLE_RECAPTCHA` | `true` in `.env.development`. |
| `REACT_APP_MOCK_AUTH` | `false` in `.env.development`. |
| `ASPNETCORE_HTTPS_PORT` / `ASPNETCORE_URLS` | Used only to choose the `/weatherforecast` proxy target. Default `http://localhost:34992`. |

`prestart` runs `aspnetcore-https.js` and `aspnetcore-react.js` (CRA ASP.NET template leftovers).

## Dev proxy

`src/setupProxy.js`:

- `/weatherforecast` → ASP.NET target above. No UI route calls it.
- A filter proxies `/loginAuth`, `/auth`, `/admin`, `/get*`, `/tables`, `/grapeProduction`, `/vineyardStatus` to the CI4 target.
- Document navigations (`Accept: text/html`) are not proxied, so `/admin/users` stays a SPA route.

Comment in the file: "CI4 backend — same-origin proxy avoids browser CORS on localhost."

Production hosting of the static `build/` folder is not defined in a Dockerfile or nginx file in this repo. **Unknown / Requires Confirmation** except for the PHP trigger below.

## Authentication mechanics

1. `POST {base}/loginAuth` with a string body and no `Content-Type`.
2. Response mapped by `mapLoginUser`: token, email, `admin_role` as string, `admin_email`, domestic flags, nested `domestic`.
3. User id is the first positive number among body fields and standard JWT payload claims (`id`, `user_id`, `sub`, …). The JWT is decoded in the browser with `atob`. It is not verified in the client.
4. Header on later calls: `Authorization: <token>` with no `Bearer ` scheme (`domesticAdmin.js` comment).
5. `GET {base}/auth/domestic/me` refreshes the domestic block. Status 404 and 403 are gaps, not logout.

Encryption (`src/utils/encryptLoginPayload.js`): CryptoJS AES-CBC, PKCS7, random 16-byte IV prepended to the ciphertext, Base64. Plaintext JSON is `{ email, password, typeOfLogin }`.

## API surface the UI calls

Domestic and admin (token required by the client):

| Method | Path | Client |
|---|---|---|
| POST | `/loginAuth` | `authStore.js` |
| GET | `/auth/domestic/me` | `domesticAdmin.js` |
| GET | `/auth/domestic/lookups` | `domesticLookups.js` |
| GET | `/auth/domestic/lookups/producers?q=` | `domesticLookups.js` |
| GET | `/auth/domestic/me/producer-codes` | `domesticLookups.js` |
| GET, POST | `/auth/domestic/help` | `domesticLookups.js`, `domesticCapture.js` |
| POST, GET | `/auth/domestic/submissions` | open or query by period and code |
| GET | `/auth/domestic/submissions/{id}` | by id |
| GET | `/auth/domestic/submissions/past?PRODKODE=` | past list |
| PUT | `/auth/domestic/submissions/{id}/saved-totals` | v2 body |
| GET, POST | `/auth/domestic/submissions/{id}/data` | back rows |
| PUT, DELETE | `/auth/domestic/submissions/{id}/data/{rowId}` | one row |
| GET | `…/reconciliation`, `…/previous-month` | capture |
| POST | `…/submit` | no body |
| GET | `/auth/domestic/admin/dashboard` | optional `period` |
| GET | `/auth/domestic/admin/submissions` | filters |
| GET | `/auth/domestic/admin/submissions/{id}` | review |
| POST | `…/approve`, `…/unlock`, `…/cancel`, `…/reopen` | review |
| POST | `/auth/domestic/admin/verify-producer-list`, `…/sync-producer-list` | tools panel |
| GET | `/auth/domestic/admin/users` | query `role`, `q` |
| GET | `/auth/domestic/admin/users/{id}` | detail |
| PUT | `…/assignment`, `…/role` | user admin |

Export and public report calls:

| Call | When |
|---|---|
| `GET /getFilters` or `GET /auth/getFilters` | export sidebar |
| `POST /getChartData`, `/getYearOverYearComparison`, `/getTotale_Liters`, `/getExports`, `/getCultivarChartData`, `/getTop25Data` | overview |
| `POST /tables/getExportMarketPerProductTypeTableData`, `getTOP25TableData`, `getExportMarketVolumeByProductTypeTableData`, `getCultivarTypeExportsTableData`, `getExportsTableData` | under `/auth` when logged in |
| `POST /auth/getPriceRangeData`, `GET/POST /auth/getPriceRangeFilterHistory`, `POST /auth/setPriceRangeFilterHistory` | price range (set is called for save and update) |
| `POST /auth/reports/getRSAExportsReport`, `GET /auth/getCountryGroup` | generated report |
| `GET /getGrapeProductionReports`, `/getGrapeProductionFilters` | grape page |
| `GET /getVineyardStatusReports`, `/getVineyardStatusFilters` | vineyard page |
| `GET` or `POST` `{base}/{item.URL}` | pivot Generate. GET for vineyard |
| `GET /getDomesticReportFilters`, `POST /getDomesticReport` | domestic reports, no auth header |

Mapper admin (`Authorization` when the auth-header flag allows):

- `GET /admin/mapper_processing_sheet/history`
- `POST /admin/mapper_processing_sheet_queue`
- `GET /admin/mapper_process_get_stats`
- `GET /admin/process-thread/{id}`
- `POST /admin/mapper_processing_retry/{id}`
- `POST /admin/mapper_processing_cancel`
- `…/admin/mapper_processing_final_sheet_test`
- `GET/POST/PUT/DELETE /admin/{endpoint}` and `/admin/{endpoint}/{id}` for `unknown-cultivar-matches`, `unknown-country-matches`, `unknown-producer-matches`, `mcc-designation-lookup`

404 handling for the domestic helper: a JSON body with `message` is a business error (`gap: false`). A 404 without a message is `gap: true` ("endpoint not built yet"). Lookups and help then return fixture objects and `fromSpec: true`.

## Front-end data model (capture)

`src/utils/domesticCaptureModel.js` is the mapping layer.

- `saved_totals.version` is `2`.
- `wine_class_totals` keys are cultivar headings when lookups provide them, otherwise `still`, `sparkling`, `fortified`, `wine_component`.
- `grand_total` is the sum of those values.
- Grid sizes use `TH`, `BT`, `HOUGRO`, `litres`.
- `resolveBt` mirrors a server type the comment names `DomesticReconciliation::resolveBt`. Sparkling or BT `V` becomes `V`. TH `B` becomes `S`. Still returns null.
- Back-row fields may be raw codes or `{ key, value }`.
- Lump rows use TH `T` and HOUGRO `TOTAL` or `WCTOTAL`.

Local draft shape is version 1 under `sawis_capture_draft_v1_*` (`captureDraftStorage.js`). It is not the API body.

## State stores

| Store | Holds |
|---|---|
| `authStore` | `user`, login, setUser, refreshDomesticProfile, autoLogin, logout |
| `filterStore` | export filters and chart/table payloads |
| `priceRangeStore` | price-range filters and history |
| `grapeProductionStore` | grape filters |
| `vineyardStatusStore` | vineyard filters |
| `domesticReportsStore` | domestic report filters |

There is no Redux and no server cache library. Capture form state sits in `ProducerCapturePanel` (`useState`), not in a store.

## Routing

| Path | Guard | Screen |
|---|---|---|
| `/` | none | `Home` |
| `/domestic/*` | `AuthRouteGuard` | lazy `DomesticRoutes` → reports (index) or capture |
| `/reports` | none | `Reports` |
| `/grape-production` | none | `GrapeProduction` |
| `/vineyard-status` | none | `VineyardStatus` |
| `/admin`, `/admin/*` | `AdminRouteGuard` | `Admin` with nested tools, users, submissions, `submissions/:submissionId` |

Nested admin guards: `AdminToolsGuard`, `DomesticUsersGuard`, `AdminSubmissionsGuard`.

`NavMenu.js` is not mounted by `Layout`. Its admin condition (`admin_role` < 0) does not affect the live navbar.

## Build, test, and quality gates

Scripts (`package.json`):

| Script | Command |
|---|---|
| `start` | `rimraf ./build && craco start` (after `prestart` ASP.NET js) |
| `build` | `cross-env NODE_OPTIONS=--max-old-space-size=8192 craco build` |
| `test` | `CI=true react-scripts test --env=jsdom` |
| `lint` | `eslint ./src/` |
| `test:e2e` | `playwright test` |
| `postinstall` | `patch-package` |

Patch: `patches/serialize-javascript+7.0.4.patch` so serialize-javascript 7 can see WebCrypto on Node 18.

Playwright (`playwright.config.js`): `testDir: ./tests`. That directory is not in the tree. `playwright-test.config.js` also exists; this note did not diff the two. README points TestSprite at `testsprite_tests/` and project name "SAWIS BI Tool - Phase 6", with the app on port 44487.

Bitbucket Pipelines (`bitbucket-pipelines.yml`): image `maven:3.3.9`, one step, SonarQube scan pipe `2.0.1`, branch filter `{master}` only. Full clone. No npm build step in that file. `phase_6` has no pipeline in this YAML.

`master_deploy.php`: if the caller IP is not in the embedded Bitbucket-style ranges, or `tkn` is missing, it stops. Known case labels (tokens omitted):

- Label → `https://lui.sawisonline.co.za/deploy.php`
- BIQA → `http://biqa.sawisonline.co.za/api/public/deploy.php`
- BI → the same BIQA URL as the BIQA case

**Requires confirmation** whether Label / BIQA / BI are the Phase 6 environments, and whether the duplicate BI target is intentional. The file contains long-lived tokens. They were not copied into Silicon Ark.

## Security notes visible in the repo (not a pentest)

- Login AES key and reCAPTCHA site key are build-time `REACT_APP_*` values, so they ship in the JS bundle when set.
- Session token is readable by any script on the origin (cookie plus `localStorage`).
- `master_deploy.php` commits deploy tokens.
- `npm` overrides record fixes for webpack SSRF, serialize-javascript RCE, SVGO, postcss, body-parser, and others (`CHANGELOG.md`, `package.json` `overrides`). Changelog says two high `react-router` RSC CSRF findings remain and need React 19 and router 8.3; the authors judged them inapplicable to this SPA.
- Authorization decisions in the UI are not sufficient. The API must enforce them. This repo cannot show that it does.
- GA4 id is hard-coded in `src/index.js`.

## Monitoring and jobs

- No application log shipper, APM, or health endpoint in this frontend.
- GA4 is the only telemetry found.
- No queue, cron, or background worker in this repository. Mapper "process thread" is an API the UI polls or opens; the worker itself is elsewhere.
- Service worker is unregistered, so Workbox packages in `package.json` are not active at runtime.

## Databases and storage

No database client in the frontend. Persistence the browser owns:

| Store | Key | Lifetime |
|---|---|---|
| Cookie | `sawisAuth` | until logout or expiry by the cookie library default (no `expires` on the auth cookie) |
| Cookie | `disclaimerAccepted` | 30 days |
| `localStorage` | `sawis_auth_v1` | until logout |
| `localStorage` | `sawis_capture_draft_v1_*` | until submit, logout, or quota failure |
| `sessionStorage` | `phase6_domestic` | tab session, also cleared on logout |

Server tables implied by field names (`domestic_sawis_7_data`, `domestic_producer_list`, mapper sheets) belong to the API. **Not defined here.**

## Related Silicon Ark project

`silicon_ark/bi_tool_php/` documents a different repository (`bi_tool_php.git`): PHP scripts that load pipe-delimited files into MySQL `sawis_bitool`. It is not this frontend. A shared SAWIS BI domain is **Inferred** from the names only. No import in this React app calls those PHP files.
