# Technical documentation — pwa (1 for the Road)

Evidence date: 2026-09-28. Source: `master` `50d7009` unless a path says otherwise. Credential values are omitted on purpose. The files that contain them are named so a reviewer can find them without this document becoming a second copy.

## Repository

| Item | Value |
|---|---|
| Remote | `git@bitbucket.org:overdrive/pwa.git` |
| Default branch | `master` at `50d7009` (2021-11-29, Russel `russel@overdrive.co.za`, "blank form validation") |
| Other branches | None. `origin/HEAD` is `origin/master` |
| First commit | `9349f17` (2021-04-02, "Initial commit") |
| Root README | None |
| Issue tracker or wiki in the repo | None |

The only long-lived line of work is `master`. Recent subjects are SMTP and form validation (29 November 2021). No later commit is on the remote.

## What kind of codebase this is

A static mobile page plus a single PHP JSON endpoint. The visual shell is a third-party PWA template (manifest short name `AppKit`, starter HTML files, `scripts/custom.js`, SCSS component library). Product behaviour is concentrated in:

- `index.html`
- `scripts/1fortheroad.js`
- `api/api.php`
- `api/config.php`

There is no framework, no Composer project, no npm project, and no test runner.

## Languages and libraries

| Piece | Where | Notes |
|---|---|---|
| HTML / CSS | `index.html`, `styles/` | Bootstrap stylesheet `styles/bootstrap.css`. Theme `styles/style.css`. Highlight CSS under `styles/highlights/` |
| SCSS | `scss/` | Source for the theme. No build script or `package.json` was found, so how CSS is regenerated is **Unknown** |
| JavaScript | `scripts/1fortheroad.js`, `scripts/custom.js`, `scripts/bootstrap.min.js` | Booking logic is plain script, not a module |
| SmtpJS | `scripts/smtp.js` | File header `SmtpJS.com - v3.0.0`. Posts to `https://smtpjs.com/v3/smtpjs.aspx` |
| PHP | `api/*.php`, `php/contact.php` | `mysqli`. No PHP version pin |
| Google Maps JavaScript | script tag in `index.html` | `libraries=places`, `v=weekly`, callback `initAutocomplete`. Key is in the tag and is omitted here |
| Fonts | `fonts/`, Google Fonts Inter | Font Awesome from `fonts/css/fontawesome-all.min.css` |
| Plugins | `plugins/` | Chart.js-style bundle, Filterizr, GLightbox, countdown, embedly, gallery views. Loaded by class name from `custom.js` |

## Application structure

```text
index.html                 booking page
_manifest.json             web app manifest
_service-worker.js         cache-first worker
_starter_*.html            template starter pages
scripts/1fortheroad.js     product script
scripts/custom.js          template behaviour and service worker registration
scripts/smtp.js            SmtpJS
scripts/bootstrap.min.js
api/api.php                live JSON API
api/config.php             database constants
api/api_old.php            older generic SQL API
api/callback.php           SnapScan poll
api/class.snapscan.php     SnapScan client
api/class.qrcode.php       QR helper used by the SnapScan class
api/dumbdb.php             SELECT * dump of table ride
api/db_functions.php       unused person-table helper
api/db_connect.php
api/Config_old.php
php/contact.php            unused contact mailer
styles/ scss/ fonts/ images/ plugins/
bitbucket-pipelines.yml
sonar-scanner.bat
```

`scripts/1fortheroad.js` is loaded after `custom.js`. It is not wrapped in `DOMContentLoaded`. `initScreens()` runs during that script's evaluation because the code calls it while assigning `document.onload`.

## Runtime request flow

1. Browser loads `index.html` and the four local scripts, then the Maps script.
2. Booking calls `fetch('api/api.php')` with `POST`, `mode: 'cors'`, and `Content-Type: application/json`.
3. Body shape:

```json
{
  "t": "<static token>",
  "d": { "a": "<action>", "...": "..." },
  "uid": 0
}
```

4. `api/api.php` reads `php://input`, checks `t` and `d`, connects with `mysqli_connect`, and switches on `d.a`.
5. Handled actions `exit` after `print json_encode(...)`. The `default` arm echoes `No Action` and then `return`s, which stops the file. The generic query builder under the switch is not reached by the four named actions or by `default`.

CORS header: `access-control-allow-origin: *`. Content-Type header on the response is `application/json; charset=utf-8; application/x-www-form-urlencoded` (both types in one header).

`display_errors` is `0`. `log_errors` is `1`. Log path is `error.log` relative to the PHP process. `api/error_log` and `api/error_log2` are committed filenames; they were not used as specification.

## API actions

| `d.a` | SQL effect | Caller |
|---|---|---|
| `one_CheckLogin` | `SELECT ClientID, strClient FROM tblClient` with `LIKE` on `strCell` | `apiRestLogin` |
| `one_GetClientDetails` | `SELECT * FROM tblClient WHERE ClientID = '<clientid>'` | None in the booking script |
| `one_BookRide` | `INSERT INTO tblBooking (...)` | `bookRide` |
| `one_BookRideWithVoucher` | Optional `INSERT INTO tblClient`, then `INSERT INTO tblBooking` | `bookRideCoupon` |

Values are interpolated into SQL strings. There are no prepared statements on these actions.

`one_BookRideWithVoucher` is the only action that returns `error` true/false with `message` (`invalid voucher`, `Could not add client`, `Could not add booking`) or `error` false with `data` set to the insert id. The other actions return `result` plus `sql`. `mysqli_query` results do not JSON-encode as row data; the login action pushes rows onto the array before adding `result` and `sql`, so a hit is a JSON object with `"0"` as the first row plus those extra keys. The client reads `data[0].ClientID`.

## Database

| Item | Value |
|---|---|
| Engine | MySQL via `mysqli` |
| Host | `sql24.jnb1.host-h.net` |
| Database name | `1fortheroad` |
| User | `DBU_OFTR` |
| Password | Present in `api/config.php`. Omitted here |
| Schema files | None |

Tables and columns that appear in SQL:

- `tblClient`: `ClientID`, `strClient`, `strCell`, `strEmail`, `txtAddressHome`, `refMasterID`, `dtRegistered`, `blnMaster`, `blnMasterPaysSubs`, `blnMasterPaysUsage`, `blnActive`, `strFirstUser`, `strLastUser`, `dtFirstEdit`.
- `tblBooking`: `refClientID`, `refDriverID`, `dateBookingDate`, `strPickupTime`, `txtPickupAddress`, `txtDeliveryAddress`, `strFirstUser`, `dtFirstEdit`, `strLastUser`, `dblCashReceived`, `strPickupPerson`, `strPickupCell`, `strTripDescription`.
- `payments`: `paymentsId`, `status` (only in `api/callback.php`).
- `ride`: only in `api/dumbdb.php`.
- `person`: `per_name`, `per_email`, `per_password`, `per_salt`, `per_date_added`, and related columns in `api/db_functions.php`.

Which of `payments`, `ride`, and `person` still exist in the live database is **Unknown**. This repo cannot create them.

## Authentication

The booking page has no login session. Gates that do exist:

| Gate | Where | What it checks |
|---|---|---|
| Static token | `api/api.php` compared with the literal in `scripts/1fortheroad.js` | Request body field `t` |
| Cellphone match | `one_CheckLogin` | `tblClient.strCell` |
| Voucher literal | `one_BookRideWithVoucher` | Equals `1FTR2021` |

`api/db_functions.php` can hash a password with salt (`hashSSHA`) for table `person`. The booking page does not use that class.

## External services

### Email

`emailNotify` calls `Email.send` with host `smtp.gmail.com`, username `info.1fortheroad@gmail.com`, recipient `bookings@1fortheroad.co.za`. The password sits in the browser script. SmtpJS forwards the payload to `smtpjs.com`. A mail password in front-end source is visible to every person who can load the script.

`php/contact.php` uses PHP `mail()` to `mail@domain.com` and is not wired to the booking page.

### Maps

`initAutocomplete` builds a `google.maps.Map` on `#map` and `google.maps.places.SearchBox` on the four address inputs. Default centre `-33.918861, 18.423300`, zoom 13, map type `roadmap`. Geolocation success overwrites `latit` and `longit`. The map is constructed once at callback time, so a late geolocation fix does not recenter an existing map. **Inferred** from the order of the calls.

### Analytics

Matomo snippet in `index.html`. Tracker host `analytics.siliconsoftware.net`. Site id `4`. Methods used: `trackPageView`, `enableLinkTracking`.

### SnapScan

`api/class.snapscan.php` documents the merchant API (`https://pos.snapscan.io/merchant/api/v1`) and QR endpoint (`https://pos.snapscan.io/qr`). Author comment: Rudi Strydom, July 2015. `api/callback.php` sets a merchant id and an API token, then `SnapScan::checkPayment`. Neither value is repeated here. Payment references are `MOMSTAXI` plus `paymentsId`.

### Web fonts

`fonts.googleapis.com` for Inter, weights 100 through 900.

## Service worker and manifest

`_manifest.json` version field is `"2.0"` with comments that it must match `data-pwa-version` on the manifest tag in `index.html`. That attribute is not on the tag. Name `1FortheRoad`, short name `AppKit`, scope `/`, display `fullscreen`, orientation `portrait`, background and theme `#000000`, start URL `index.html`. Every icon entry points at `images/logoNew.png` while declaring sizes from 72 to 512. `app/icons/` holds separate PNG sizes (`icon-72x72.png` through `icon-512x512.png`) and `app/splash/` holds iPhone splash images. The manifest does not reference `app/icons/` or the splash files.

`_service-worker.js`:

- Cache name `Cache-Version-v1`.
- Install: `cache.addAll(['index.html'])`, then `skipWaiting()`.
- Fetch: cache match, otherwise `fetch`.
- Activate: `clients.claim()`.

`scripts/custom.js` registers `/_service-worker.js` with scope `/` when `isPWA === true` (it is `true`). `pwaRemind` is `1` day. `pwaNoCache` is `false`. Install UI selectors are `#menu-install-pwa-android` and `#menu-install-pwa-ios`, which are commented out in `index.html`, so the prompt loop does not find windows to open. The worker registration does not depend on those elements.

`?app` on the page URL writes `1FortheRoad-PWA-Timeout-Value` in `scripts/1fortheroad.js` as well as the template's own timeout key of the same name.

## Deployment

`bitbucket-pipelines.yml`:

- Image `python:3.5.7`.
- Branch filter: `master` only.
- One step: `apt-get update`, install `zip`, `git`, `git-ftp`, then `git ftp push -u "$FTP_USERNAME" -p "$FTP_PASSWORD" ftp://$FTP_HOST`.
- `git ftp init` is commented (commit `ea503ba`, "change init to push").

Commit `60c4a92` message: "init - pipeline setup app.1fortheroad.co.za". The host name is not in the YAML. The three FTP variables are not in the repo. Whether the pipeline is still enabled, and which host it pushes to, is **Unknown / Requires Confirmation**.

No Docker file, no server config, no `.htaccess`, and no environment sample file.

## Local developer files

| Path | What it is |
|---|---|
| `sonar-scanner.bat` | SonarScanner CLI 4.6.2.2472 Windows launcher. Expects SonarScanner under `C:\sonar-scanner`. Not wired to Bitbucket Pipelines |
| `.idea/` | JetBrains project directory, committed |
| `.DS_Store` | Finder metadata, committed at the root, under `app/`, and under `php/` |

`sonar-project.properties` and `sonarqube_scan.bat` were listed when the onboarding task was created. They are not in this clone.

## Security notes (observed, not a pentest)

- Database password, Gmail SMTP password, SnapScan API token, and the Maps key are stored in the repository. The SMTP password and the Maps key are in files the browser downloads.
- The API token is the same in PHP and in the public script.
- SQL is concatenated from request fields. Responses echo the SQL.
- `Access-Control-Allow-Origin` is `*`.
- `api/dumbdb.php` and `api/api_old.php` are in the web tree if the FTP push publishes the whole repository. The booking page does not link to them. What the server actually exposes is **Unknown / Requires Confirmation**.
- `api/api.php` connect failures print `mysqli_connect_error()` to the client.
- No rate limit, captcha, or session expiry was found on the booking actions.

## Development and testing

No test directory, PHPUnit config, or CI test step exists. The pipeline only FTPs `master`. Manual checks implied by commit messages ("test map", "testing email sending", "test secure token", "update Apple store test") were not left as scripts.

`scripts/custom.js` contains the comment "Check Documentation folder for detailed explanations". No `Documentation` folder is in the repository.

## Background work

No queue, cron entry, or worker process is defined in the repo. `api/callback.php` is a request-driven poll: it runs when something requests that URL. Nothing in the repo schedules it.

## Monitoring

The only monitoring client in the page is the Matomo snippet. No health check, log shipper, or uptime config is in the repo. PHP errors are written to a local `error.log` when PHP runs `api/api.php`.
