# PRD — pwa (1 for the Road)

Evidence date: 2026-09-28. Source: `git@bitbucket.org:overdrive/pwa.git`, `master` `50d7009` (2021-11-29).

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

No formal PRD existed in the repository or in `silicon_ark/pwa/` before this file. Requirements below are recovered from the page, the booking script, and the PHP API. They are not a new product decision.

Credential values are omitted. The files that hold them are named where the behaviour depends on their presence.

## Product purpose

A person opens a mobile page titled **1FortheRoad** and books a lift. They identify themselves with a mobile number, then either book as a member already stored as a client, or book as a non-member with a voucher code. The server writes a booking. The browser emails a trip summary to the bookings mailbox. The success panel tells the person they will receive a confirmation SMS.

**Implemented** in `index.html` and `scripts/1fortheroad.js`. The SMS sentence is copy only: no SMS integration exists in this repository. Provenance of the SMS promise: **Documented** on the page, **not implemented** here. Whether another system sends that SMS is **Unknown / Requires Confirmation**.

## Users and roles

| Role | Where it appears | What the code lets them do | Provenance |
|---|---|---|---|
| Visitor | `index.html` `#loginScreen` | Enter a mobile number and choose Member or Non Member | **Implemented** |
| Member | Button "Member" calls `loginVal()` | If `tblClient.strCell` matches, the member booking form is the intended next screen | **Implemented** lookup. The screen switch is **Inferred** to fail; see FR-003 |
| Non-member | Button "Non Member" calls `loginValCop()` | Opens the voucher booking form after a non-blank mobile number | **Implemented** |
| Bookings mailbox | `emailNotify()` | Receives subject "New Trip" at `bookings@1fortheroad.co.za` | **Implemented** |
| Driver | `refDriverID` inserted as `'-1'` | No driver UI in this repo | **Inferred** unassigned. A driver product is **Unknown** |
| Staff / web-admin | Comment in `one_BookRideWithVoucher` | Not in this repository | **Planned** in a comment only |

There is no password, OTP, session, or role table in the booking flow. Member identity is a cellphone match. Provenance: **Implemented**.

## Major workflows

### 1. Open the page

Trigger: browser loads `index.html`.

Inputs: optional query parameter `app`.

Behaviour:

- Title is `1FortheRoad`. Manifest link is `_manifest.json`.
- Matomo records a page view for site id `4` on `//analytics.siliconsoftware.net/`.
- `scripts/custom.js` hides `#preloader` after 150 ms and, when `isPWA` is true, registers `/_service-worker.js` at scope `/`.
- `document.onload = initScreens()` calls `initScreens()` immediately and assigns its return value (`undefined`) to `document.onload`. `initScreens` hides `#alreadyBooked`, `#copounBooked`, and `#otherNumber`.
- If `app` is present on the URL, the script writes `Date.now()` into `localStorage` key `1FortheRoad-PWA-Timeout-Value`.
- The browser is asked for geolocation. The default map centre before a fix is latitude `-33.918861`, longitude `18.423300`.
- Google Maps loads with `callback=initAutocomplete` and the Places library, and attaches SearchBox widgets to `#pickup`, `#dropoff`, `#pickupVoucher`, and `#dropoffVoucher`.

Output: the login card. The install menus `#menu-install-pwa-android` and `#menu-install-pwa-ios` are HTML comments, so the install prompt in `custom.js` has no elements to show (`pwaWindows.length` is 0). The service worker can still register.

Error: if geolocation is missing, the script sets `x.innerHTML`. `x` is not defined. **Inferred** `ReferenceError` on that branch only. The Maps call and the API are not blocked by that.

### 2. Member identifies a mobile number

Trigger: Member button, `loginVal()`.

Inputs: `#mobileNumber`.

Validation: empty, null, or undefined adds class `customBorderBottom` on `#mobileNumberDiv` and does not call the API. Any other string is accepted, including non-numeric text, because the check is only emptiness. The input type is `number`.

Behaviour when non-empty: `cellphone` is set and `apiRestLogin("one_CheckLogin", mobileNumber)` POSTs JSON to `api/api.php`.

Server (`one_CheckLogin`):

- Strips a leading `0` from the cellphone (`ltrim`).
- Runs `SELECT ClientID, strClient FROM tblClient WHERE REPLACE(strCell, ' ', '') LIKE '%<cellphone>%'`.
- Returns matching rows plus `result` and `sql`. Zero rows set `result` to the string `"0 results"`.

Client when `data['result'] == "0 results"`: shows `#otherNumber` with the text "Please enter registerd number , or use Non Member with your active coupon code".

Client when rows exist: reads `data[0].ClientID` into `clientid` and `data[0].strClient` into `pickupName`, then uses `loginScreen`, `alreadyBooked`, `responseResult`, and `copounBooked`. Those names are declared with `let` inside `loginVal` and `loginValCop`. They are not globals, and `apiRestLogin` does not receive them. **Inferred** from JavaScript scope: the member-found branch throws `ReferenceError` before it can show "Welcome back" or the booking form. This was not executed in a browser for this document. **Requires confirmation** against a running copy, in case production differs from `50d7009`.

Expected result the code is trying to reach: hide login, show `#alreadyBooked`, prefill `#time` as `hours + "h" + minutes` and `#date` as `YYYY-MM-DD` from `toISOString()`.

Error: fetch failures are logged and leave the person on the login screen. The API echoes the MySQL connect error string if `mysqli` cannot connect. SQL text is included in the JSON body.

### 3. Member books a lift

Trigger: Book Lift on the member card, `bookRide()`.

Inputs: `#pickup`, `#dropoff`, `#time`, `#date`, plus globals `clientid`, `pickupName`, `cellphone`.

Validation: pickup and drop-off must be non-empty. Time and date are not checked in script. Empty pickup or drop-off calls `loginVal()` again (another login request) and marks `#pickupDiv` / `#dropoffDiv`.

Behaviour when both addresses are non-empty: POST action `one_BookRide` with `dblCashReceived` `0`, `strPickupPerson` from `pickupName`, `strPickupCell` from `cellphone`, and `strTripDescription` `''`.

Server inserts into `tblBooking` with `refDriverID` `'-1'`, `strFirstUser` and `strLastUser` `'app'`, and `dtFirstEdit` `CURRENT_TIMESTAMP`. It does not check that `clientid` is a real client. The JSON includes `result` (the mysqli result, which JSON-encodes poorly for a result object) and `sql`.

Client: sets `#responseResult` to visible immediately, and again inside the JSON `then`. The `then` writes `#rideSummary` and calls `emailNotify`. It does not read an error flag. A failed insert that still returns JSON still shows SUCCESS and still sends the email.

Output the page claims: a green SUCCESS card, summary text, and "you will receive a confirmation SMS shortly". CLOSE calls `closeClear()`, which reloads the page.

Error: network or JSON parse failure is only `console.log`. The SUCCESS card is already visible because that line sits outside the promise.

### 4. Non-member opens the voucher form

Trigger: Non Member button, `loginValCop()`.

Inputs: `#mobileNumber`.

Validation: same emptiness check as the member button. A non-empty number does not call `one_CheckLogin`.

Behaviour: stores `cellphone`, hides login, member form, and result, shows `#copounBooked`, and prefills `#timeVoucher` and `#dateVoucher` the same way as the member form.

Output: heading "Book a Lift: Voucher Client".

### 5. Non-member books with a voucher

Trigger: Book Lift on the voucher card, `bookRideCoupon()`.

Inputs: `#nameSurnameVoucher`, `#pickupVoucher`, `#dropoffVoucher`, `#timeVoucher`, `#dateVoucher`, `#Voucher`, and `cellphone` from the previous step.

Validation: name, pickup, and drop-off must be non-empty. The voucher field, time, and date are not in the client check. On failure the script calls `loginValCop()` and marks the empty fields.

Behaviour when the three fields are filled: POST action `one_BookRideWithVoucher`. The payload sets `voucher` twice. The first value is the identifier `pickup`, which is not declared in this function. The second value is `#Voucher`, so the second property wins. `name` and `surname` are both the single name field. Cash is `0`. Trip description is `''`. `clientid` is sent and ignored by this server action.

Server:

- Accepts the voucher only when it equals the literal `1FTR2021`. Any other value returns `{ error: true, message: "invalid voucher" }` inside `result`. The comment above the check says the voucher section needs to be built in web-admin.
- Looks up `tblClient` by cellphone with the same `LIKE` pattern, without stripping a leading zero (unlike `one_CheckLogin`).
- If one or more rows match, `ClientID` becomes the last row in the loop. The posted name is not written back.
- If none match, inserts `tblClient` with `refMasterID` `0`, email `''`, home address equal to the drop-off, `blnMaster`, `blnMasterPaysSubs`, `blnMasterPaysUsage`, and `blnActive` all `'1'`, and `strFirstUser` / `strLastUser` `'app'`.
- Inserts `tblBooking` the same way as the member action.

Client: same SUCCESS panel and the same email, including when `result.error` is true. The summary uses the form fields, not the server id.

### 6. Email the bookings desk

Trigger: JSON parse success inside `bookRide` or `bookRideCoupon`.

Behaviour: `Email.send` from SmtpJS posts to `https://smtpjs.com/v3/smtpjs.aspx`. Host is `smtp.gmail.com`. Username and From are `info.1fortheroad@gmail.com`. To is `bookings@1fortheroad.co.za`. Subject is "New Trip". Body is the trip text passed in. The password is in the script and is not copied here.

Output: SmtpJS response logged to the console. The SUCCESS panel does not wait for it and does not show a mail failure.

`php/contact.php` is a separate template mailer to `mail@domain.com`. `index.html` does not post to it.

## Functional requirements

### FR-001 — Show the login card first

Requirement: On load, the person sees the login card (logo, "Login", mobile number, Member, Non Member). The member form, voucher form, unknown-number message, and success panel are not shown.

Who: visitor. Trigger: page load. Inputs: none.

Expected behaviour: `initScreens()` hides `#alreadyBooked`, `#copounBooked`, and `#otherNumber`. `#responseResult` starts with `display:none`.

Output: login card. Validation: none. Error: if those ids are missing, `initScreens` throws.

Evidence: `index.html`, `initScreens` in `scripts/1fortheroad.js`. Implementation: **Implemented**. Confidence: high.

### FR-002 — Reject a blank mobile number

Requirement: Member and Non Member do nothing to the API when the mobile number is blank, and mark the field.

Who: visitor. Trigger: either button. Inputs: `#mobileNumber`.

Expected behaviour: add `customBorderBottom` on `#mobileNumberDiv`. Member does not call `one_CheckLogin`. Non Member does not open the voucher form.

Output: highlighted field. Error: none defined beyond the highlight.

Evidence: `loginVal`, `loginValCop`. Implementation: **Implemented** (commit `50d7009`). Confidence: high.

### FR-003 — Find a member by cellphone

Requirement: A non-blank Member submit looks up `tblClient` by cellphone and, on a hit, opens the member booking form addressed with `strClient`.

Who: member. Trigger: Member button. Inputs: mobile number.

Expected behaviour on the server: strip one leading `0`, match `REPLACE(strCell, ' ', '')` with `LIKE %value%`, return `ClientID` and `strClient`.

Expected behaviour on the page, as written: on `"0 results"`, show the red "registerd number" sentence. On a hit, set `clientid` and `pickupName`, show "Welcome back" plus the name, show `#alreadyBooked`, prefill time and date.

Discrepancy: the hit branch references `loginScreen` and the other screen variables from `apiRestLogin`, where they are not in scope. **Inferred** runtime `ReferenceError`. The lookup request itself is still sent.

Validation: no length, country, or OTP check. Error: API and fetch errors are logged; connect failures return a MySQL error string.

Evidence: `loginVal`, `apiRestLogin`, `one_CheckLogin`. Implementation: lookup **Implemented**; screen transition **Inferred broken**. Confidence: high that the scope bug is in the source; medium until a browser run confirms it.

### FR-004 — Book a lift for a known client id

Requirement: The member form inserts a `tblBooking` row for `clientid` with pickup, drop-off, date, and time.

Who: member. Trigger: Book Lift. Inputs: pickup, drop-off, time, date, `clientid`.

Expected behaviour: empty pickup or drop-off is marked and `loginVal()` runs again. Otherwise POST `one_BookRide`. Server inserts the row with driver `-1`, cash `0`, pickup person and cell from the login step, empty trip description, user `'app'`.

Output: SUCCESS panel and an email. The panel is shown even when the insert fails, as long as the script reached the POST, and again when JSON parses.

Validation: addresses non-empty only. Date input has HTML `min="2021-01-01"` and `max="2030-01-01"`, then the script overwrites the value with today's UTC date when the form is opened. Time is free text.

Error: see the success-panel note. No user-visible API error.

Evidence: `bookRide`, `one_BookRide`. Implementation: **Implemented**. Confidence: high.

### FR-005 — Open the voucher form without a client lookup

Requirement: Non Member with a non-blank mobile number shows the voucher form and does not require an existing client.

Who: non-member. Trigger: Non Member button. Inputs: mobile number.

Expected behaviour: show `#copounBooked`, prefill time and date, keep the number for the later POST.

Output: voucher card. Error: blank number only.

Evidence: `loginValCop`. Implementation: **Implemented**. Confidence: high.

### FR-006 — Accept only one voucher code and create client plus booking

Requirement: A voucher booking is stored only when the voucher equals `1FTR2021`. A new `tblClient` row is inserted when the cellphone is not found. A `tblBooking` row is inserted for the resolved client id.

Who: non-member. Trigger: Book Lift on the voucher card. Inputs: name, pickup, drop-off, time, date, voucher, cellphone.

Expected behaviour: client requires name, pickup, and drop-off. Server rejects any voucher other than `1FTR2021` with message `invalid voucher`. Existing cellphone reuses the last matching `ClientID` and does not update the name. New client stores an empty email and sets the home address to the drop-off. Flags `blnMaster`, `blnMasterPaysSubs`, `blnMasterPaysUsage`, and `blnActive` are stored as `1`. What those flags mean in the business is **Unknown**.

Output: SUCCESS panel and email even when the server message is `invalid voucher`, because the client does not read `result.error`.

Error conditions the server does encode: `invalid voucher`, `Could not add client`, `Could not add booking`.

Evidence: `bookRideCoupon`, `one_BookRideWithVoucher`. Implementation: **Implemented**. The comment that a web-admin voucher section is still to be built is **Planned** and not in this repo. Confidence: high.

### FR-007 — Email the trip summary

Requirement: After the booking JSON parses, the browser emails the trip text to the bookings address.

Who: browser, on behalf of the person who booked. Trigger: `then` of the booking fetch. Inputs: the summary string built from the form.

Expected behaviour: SmtpJS send via `smtp.gmail.com` from `info.1fortheroad@gmail.com` to `bookings@1fortheroad.co.za`, subject "New Trip".

Output: console log of the SmtpJS message. No on-screen mail status.

Error: mail failure does not change the SUCCESS panel.

Evidence: `emailNotify`, `scripts/smtp.js`. Implementation: **Implemented**. Confidence: high for the code path. Whether the Gmail account and SmtpJS still accept the call is **Unknown**.

### FR-008 — Tell the person an SMS is coming

Requirement: The success card says a confirmation SMS will arrive shortly.

Who: person who reached the success card. Trigger: success panel display.

Expected behaviour in copy: an SMS. Expected behaviour in this repo: no SMS is sent.

Output: the sentence on `#responseResult`.

Evidence: `index.html`. Implementation of the sentence: **Documented** on the page. Implementation of an SMS gateway: absent. Confidence: high that this repo does not send SMS. **Requires confirmation** if another system does.

### FR-009 — Places suggestions on address fields

Requirement: Pickup and drop-off fields on both forms are Google Places search boxes, and a map is drawn for the member section.

Who: person on a booking form. Trigger: Maps script callback `initAutocomplete`.

Inputs: the four address inputs and `#map`. Default centre as in workflow 1. Zoom 13, `roadmap`.

Expected behaviour: choosing a place drops a marker and fits the map bounds. The listener that would bias search to the current map bounds is commented out.

Output: markers on `#map`. The map element is inside `#alreadyBooked`, which is hidden until a member login succeeds.

Error: if the Maps script fails, autocomplete does not run. Address fields still accept typed text.

Evidence: `initAutocomplete`, the Maps script tag in `index.html`. Implementation: **Implemented**. Confidence: high. The API key is embedded in the page and is omitted here.

### FR-010 — Installable web app shell

Requirement: The page is a fullscreen portrait web app named 1FortheRoad, able to cache `index.html`, with home-screen install instructions.

Who: mobile visitor. Trigger: load, and the install timers in `custom.js`.

Expected behaviour: manifest name `1FortheRoad`, `display` `fullscreen`, `start_url` `index.html`, `orientation` `portrait`, background and theme `#000000`. Service worker cache name `Cache-Version-v1` caches `index.html` only and serves the cache hit before the network. `custom.js` sets `isPWA = true` and registers the worker.

Discrepancy: install markup is commented out (commit message "update hiding install screens", `4fd2a24`). `short_name` remains `AppKit`. Manifest comments say `version` must match `data-pwa-version` in `index.html` or an update loop occurs. `index.html` has no `data-pwa-version`. **Inferred** the version-loop warning is leftover template text.

Evidence: `_manifest.json`, `_service-worker.js`, `scripts/custom.js`, commented blocks in `index.html`. Implementation: worker and manifest **Implemented**; install UI **removed from the page** (commented). Confidence: high.

### FR-011 — Record a page view

Requirement: Each load sends a Matomo page view.

Who: visitor browser. Trigger: inline script in `index.html`.

Inputs: site id `4`, tracker `//analytics.siliconsoftware.net/matomo.php`.

Output: Matomo request. A `noscript` image beacon repeats the same site id.

Error: none handled in the page.

Evidence: `index.html` head. Implementation: **Implemented**. Confidence: high. Whether site id `4` is still this product is **Unknown / Requires Confirmation**.

## Integrations

| System | Purpose | Trigger | Direction | Auth as found | Response / failure |
|---|---|---|---|---|---|
| MySQL `1fortheroad` | Clients and bookings | API actions | App to database | User and password in `api/config.php` | Connect error string returned to the client. Query errors become a false `result` or an error message on the voucher path |
| SmtpJS / Gmail | Trip email | Booking JSON parsed | Browser to `smtpjs.com`, then Gmail | Username and password in `emailNotify` | Console log only |
| Google Maps Places | Address search and map | Page load | Browser to Google | Key in the script URL | Autocomplete does not start if the script fails |
| Matomo | Page analytics | Page load | Browser to Silicon Software analytics | Site id `4`, no token in the page | Unhandled |
| SnapScan | Payment status poll | Request to `api/callback.php` | Server to `https://pos.snapscan.io` | Merchant id and API token in `callback.php` | Not called by the booking page. Reference prefix `MOMSTAXI` |
| Bitbucket Pipelines / FTP | Deploy `master` | Push to `master` | Pipeline to FTP host | `$FTP_USERNAME`, `$FTP_PASSWORD`, `$FTP_HOST` | Pipeline file has no failure notification step |

`one_GetClientDetails` selects `*` from `tblClient` by `clientid`. Nothing in `scripts/1fortheroad.js` calls it. Provenance: **Implemented** on the server, unused by this page.

## Constraints

- One static request token must equal the value checked in `api/api.php` before any action runs. The same literal is hardcoded in `scripts/1fortheroad.js`. A wrong token returns `{'message':'no action'}`.
- The API allows `Access-Control-Allow-Origin: *`.
- Queries are built by concatenating request fields into SQL. The JSON body includes the SQL text on the login and booking actions.
- `display_errors` is off for `api/api.php`. Errors are logged to `error.log` beside the script (`ini_set("error_log", "error.log")`). Files `api/error_log` and `api/error_log2` are in the tree; their contents were not used as requirements.
- Booking date widgets cannot be set in the HTML control outside 2021-01-01 and 2030-01-01 until script overwrites them.
- Service worker caches only `index.html`. API calls are not listed in `REQUIRED_FILES`, but a cache-first fetch handler still returns a cached response for any previously cached URL.
- No automated test suite gates a change.

## Data the product writes

| Store | Written by | Fields the INSERT or lookup names |
|---|---|---|
| `tblClient` | Voucher booking when the cell is new | `refMasterID`, `strClient`, `strEmail`, `strCell`, `txtAddressHome`, `dtRegistered`, `blnMaster`, `blnMasterPaysSubs`, `blnMasterPaysUsage`, `blnActive`, `strFirstUser`, `strLastUser`, `dtFirstEdit` |
| `tblBooking` | Both booking actions | `refClientID`, `refDriverID`, `dateBookingDate`, `strPickupTime`, `txtPickupAddress`, `txtDeliveryAddress`, `strFirstUser`, `dtFirstEdit`, `strLastUser`, `dblCashReceived`, `strPickupPerson`, `strPickupCell`, `strTripDescription` |
| `localStorage` | Template and `?app=` | Keys prefixed `1FortheRoad-` (theme, highlight, PWA timeout, PWA prompt) |
| Browser console | API and mail | Response bodies, which include SQL |

Schema for columns that are never named in SQL is **Unknown**. No `CREATE TABLE` is in the repository.

## Out of scope for the booking page, but present in the tree

- `api/api_old.php` generic `select` / `insert` / `update` / `delete`. Not loaded by `index.html`.
- `api/dumbdb.php` reads a table named `ride` and builds INSERT statements. Not linked from the page.
- `api/db_functions.php` is an AndroidHive-style `person` user store (`storeUser`, `getUserByEmailAndPassword`). It `require`s `Config.php`, while the file in the tree is `config.php`. Not referenced by `api.php`.
- `api/callback.php` SnapScan payment poll against `payments`.
- `php/contact.php` and the four `_starter_*.html` files.
- `plugins/` (charts, countdown, embedly, filterizr, galleryViews, glightbox) loaded by `custom.js` only when a trigger class exists. The booking page does not use those triggers.

The task-creation file list included `plugins/nscripts`. That directory is not in this clone. **Unknown** whether it was untracked on the scan host.
