# Technical documentation — indawo_65

Evidence date: 2026-10-12. Revision: `INDAWO_84_2026` `08157c5` (2026-03-24, "f ixed cronjob syncs").

This repository has no `docs/` directory. `README.md`, `CHANGELOG.md`, `NewKnowledgeBase.md`, and `setup_composer.md` are the written notes, and they stop at 2026-02-11 except `setup_composer.md`, which has no date. This file records the tree that was read. It does not repeat credentials.

## Stack

| Layer | Choice | Evidence |
|---|---|---|
| Language | PHP `>=8.4` | `composer.json`, `setup_composer.md` |
| Style | Page-at-a-time scripts, not a router framework. Shared kernel is the Nemo classes | Root `*.php`, `_nemo*.cls.php` |
| App database | MySQL. Name `indawo_online` on most host cases. Name `sil27_indawo_online` on `silicon-overdrive.co.za` | `system.php` |
| Back office | Microsoft SQL Server via ODBC (`odbc_connect` / `odbc_exec` in the SQL Server Nemo class) | `_nemo.database.SQL_SERVER.cls.php`, `setup_composer.md` |
| Config | `vlucas/phpdotenv` `safeLoad()` of a gitignored `.env`. Template file is `env`. Only local and default host cases read `DB_*` | `system.php`, `env` |
| MFA | `robthree/twofactorauth` `^3.0`, `endroid/qr-code` `^6.1`, `ext-gd` | `composer.json`, `login.php` |
| PDF | Composer `mpdf/mpdf` `^8.2` and `spipu/html2pdf` `^5.3`. Vendored `mpdf/` and `html2pdf/` are also in the tree | Those paths, `composer.json` |
| Excel | Vendored `PHPExcel/` | **Implemented** in the tree. Call sites were not fully listed |
| Charts | Vendored Highcharts and canvg under `plugins/` and `charts/` | **Implemented** |
| Web server | `setup_composer.md` says Apache or Nginx should already be installed. No `.htaccess` is in this checkout | **Documented** expectation. No in-repo rewrite rules |
| CI | Bitbucket Pipelines, SonarQube pipe `sonarsource/sonarqube-scan:3.0.2`, full clone | `bitbucket-pipelines.yml` |
| Static analysis key | `sonar.projectKey=overdrive_indawo_65_fbe8b635-8a1f-460d-ad73-ab11cacca02f` | `sonar-project.properties` |

Composer package name: `russel-sauer/indawo`. There is no `require-dev` block and no PHPUnit dependency. `test/` contained no files in this checkout.

`composer.json` says: do not edit `composer.lock` by hand, do not copy it from another project, do not commit `vendor/`, use `composer install` in production, and do not use `composer update` on production.

The same Sonar project key is recorded in the Silicon Ark set for repository `indawo_84`. Two repositories sharing one Sonar key is **Implemented** as identical key text. Whether both projects still upload to that key is **Unknown / Requires Confirmation**.

## Application structure

```text
indawo_65/
  index.php, login.php, home.php          entry and session gate
  *.php                                   one script per screen or cron
  includes/*.cls.php                      screen classes (Nemo list / details)
  includes/reports/                       report renderers
  _nemo*.cls.php                          framework: DB, list, details, menu, email, report, password
  _sync.cls.php, _sync.php                batch Sync and per-record SyncRecord
  system.php                              dotenv, host switch, mysqli, session path
  system.functions.inc.php                shared helpers
  project.functions.inc.php               company context and domain helpers
  layouts/                                header and layout includes
  MFATables.sql                           MFA columns and tables
  includes/UserAccountDeactivation.sql    two email-template inserts
  mpdf/, PHPExcel/, html2pdf/, plugins/   vendored libraries
  env                                     commented environment template
```

A typical screen includes the Nemo bootstrap (through `system.php`) and uses `$_SESSION["ONLINE"]`. Company-scoped SQL calls `getOWC()` or `whereOWC()`. That pattern was confirmed on `index.php`, `login.php`, `job.php`, and `order.php`, not on every page.

Nemo classes on the root:

| File | Role |
|---|---|
| `_nemo.cls.php` | Front include used by crons |
| `_nemo.basic.cls.php` | Page object, settings, modules |
| `_nemo.database.cls.php`, `_nemo.database2.cls.php` | MySQL |
| `_nemo.database.SQL_SERVER.cls.php` | ODBC |
| `_nemo.list.cls.php`, `_nemo.list.basic.cls.php` | Lists, sort, pager |
| `_nemo.details.cls.php` | Detail forms |
| `_nemo.menu.cls.php`, `_nemo.menu.basic.cls.php` | Menus |
| `_nemo.email.cls.php` | Email |
| `_nemo.report.cls.php` | Reports |
| `_nemo.password.cls.php` | Password hash, history, expiry |
| `_nemo.control.cls.php` | Controls |
| `_sync.cls.php` | `Sync` batch and `SyncRecord` push |

`SyncRecord::Save`, `Delete`, `UpdateJobPercComplete`, and `UpdateJobCMCodes` are the push methods. `Sync::Execute` is the batch. A comment in `SyncRecord` says `eval` of `LocalEvalFROM` fails on dev because `odbc_*` is missing. **Implemented** comment.

## Host switch (`system.php`)

Dotenv loads first for every request. `www.silicon-overdrive.co.za` is normalised to `silicon-overdrive.co.za`.

| `SERVER_NAME` | MySQL database | SQL Server (no secrets) | Session path | Notes |
|---|---|---|---|---|
| `debian` | `indawo_online` | FreeTDS, database `CLIENTS` (commented alternative `ind_INDAWO`) | `DOCUMENT_ROOT/REPO_Indawo/SESSION/` | Comment: dev repo local |
| `lamp` | `indawo_online` on localhost | `SQL-SERVERR2`, database `ind_INDAWO` | `DOCUMENT_ROOT/indawo_online/SESSION/` | |
| `silicon-overdrive.co.za` | `sil27_indawo_online` on `cmysql5-5.wadns.net:3306` | Not set in this case | `DOCUMENT_ROOT/indawo_online/SESSION/` | Comment: WebAfrica |
| `192.168.100.31` | `indawo_online` on localhost | `SQL-SERVERR2`, database `ind_INDAWO` | `DOCUMENT_ROOT/indawo_online/SESSION/` | |
| `online.indawo.local` | `indawo_online` on localhost | `CPTSQL02` port `49834`, database `CLIENTS` | `DOCUMENT_ROOT/SESSION/` | `SERVER_PROTOCOL` HTTPS. Comment calls it live |
| `online.indawo.biz` | `indawo_online` | Effective host name `CPTSQL02` with no driver string | `DOCUMENT_ROOT/SESSION/` | Case assigns settings twice. The second block wins: MySQL host name `database`. Comment calls it live |
| `192.168.0.28` | `indawo_online`, host name `database` | Host name `CPTSQL02` | `DOCUMENT_ROOT/SESSION/` | HTTPS |
| `localhost` | `envv("DB_DATABASE", "indawo_online")` | `envv("HOSTNAME_SQL", ODBC Driver 18 to CPTSQL02 port 1433, database CLIENTS, TrustServerCertificate=yes)` | `__DIR__/SESSION/` | Displays errors except notice, warning, and deprecated. Logs to `DOCUMENT_ROOT/error.log`. SQL user and password have source fallbacks |
| `127.0.0.1` | Same MySQL `envv` defaults | Not set | `__DIR__/SESSION/` | Same error display as localhost |
| default | Writes the localhost MySQL `envv` block and sets `SERVER_NAME` to `localhost` | Not set | `__DIR__/SESSION/` | Unknown hosts become localhost without SQL fields |

Non-local cases store passwords as literals. Those values are omitted. `setup_composer.md` tells production to put secrets in `.env` and `chmod 640` the file, and to install under `/var/www/indawo`. The host switch does not read `.env` for `online.indawo.biz`. **Documented** setup guide versus **Implemented** switch. **Requires Confirmation** which host name production actually sends.

`setup_composer.md` ODBC steps: system DSN name `CPTSQL02`, server `192.168.0.9,53950`, SQL authentication, trust server certificate, then open `/odbc_connect.php`.

After the switch, `session.save_path` is the case's `sessionpath`. If the case sets `SERVER_PROTOCOL`, it overwrites `$_SERVER["SERVER_PROTOCOL"]`.

## Environment template (`env`)

All keys are commented. Names only:

- `APP_ENV` (comment lists local, development, qa, production)
- `SERVER_PROTOCOL` (`HTTP` or `HTTPS`)
- `DB_HOSTNAME`, `DB_USERNAME`, `DB_PASSWORD`, `DB_DATABASE`
- `HOSTNAME_SQL`, `USERNAME_SQL`, `PASSWORD_SQL`

There is no `realTimeSync` key in this template. That key exists in the later `indawo_84` template, not here.

## Authentication data

`MFATables.sql`:

- `sysUser.blnMFAEnabled` and `blnMFARequired`, both `TINYINT(1)` default 0.
- `tblUserMFA`: one secret per `refUserID` (unique), timestamps.
- `tblUserMFARecoveryCodes`: code, used-at.
- `tblUserMFARememberedDevices`: SHA-sized `strTokenHash` unique, user agent, IP, `dtExpiresAt`.

Foreign keys point at `sysUser(UserID)`.

Password rows are `sysPassword` (type, user id, salt, hash, `dtPassword`, and `dtPasswordExpiry` when the column exists). Hash is SHA-256 of salt plus password (`_nemo.password.cls.php`). The expiry column ALTER in that class is commented out. **Implemented** comment. Whether the column exists on each database is **Unknown / Requires Confirmation**.

Login history is `sysLogin` (`strDateTime`, `strType`, `strResult`, `strUsername`). Deactivation and the usage log read it. Successful results used by deactivation are the strings `Login successful` and `Login successful (MFA verified)`.

## Sync data flow

```text
SQL Server (ODBC)
  -> Cron.daily.1 through Cron.daily.5
  -> staging tables, then MySQL tbl*
  -> Cron.daily.6 data dumps

Job suggestion Accept
  -> UPDATE tblForeman
  -> SyncRecord Foreman UPDATE
  -> SQL Server
```

Cron 1 also rotates `LoginPIN` on the first of the month. See business rule BR-005.

There is no queue table, no morning catch-up, and no evening retry in this tree.

`cron.CashFlow.php` writes PDFs when the day is Monday or the 23rd. Output under `Cash Flow Report/` is gitignored by the pattern `'Cash Flow Report'/`.

## Pipeline and analysis

`bitbucket-pipelines.yml` defines one step, SonarQube analysis, on branches matching the pattern `{master}`. Variables `SONAR_HOST_URL` and `SONAR_TOKEN` come from the repository or workspace. Clone depth is full. Cache is `~/.sonar`.

`INDAWO_84_2026` is the branch that contains the 2026 application work. `master` at `80c67f5` (2026-01-29) is an ancestor and is the branch the pipeline pattern names. Whether Bitbucket runs this pipeline on other branches is **Unknown / Requires Confirmation**.

`sonar-project.properties` sets only the project key.

## Security notes (no secret values)

- Named-host database passwords and SQL passwords are plaintext in `system.php`.
- `localhost` SQL `envv` fallbacks are non-empty in source.
- Deactivation email recipients are hardcoded, with a "for testing" comment.
- Login PIN is a 4-character value emailed to all active users and stored in `sysSettings`.
- `JobSuggestion::Accept` concatenates a field name and a value into SQL.
- `odbc_connect.php` and `odbc_connect_2.php` are web-reachable PHP files in the tree. No `.htaccess` denies them.
- `.gitignore` ignores `.env*`, `SESSION/`, order and invoice output folders, `phpinfo.php`, and several `test*.php` names. It does not ignore `system.php`.
- Localhost and the default host turn `display_errors` on and drop notices, warnings, and deprecations from `error_reporting`.

These are observations from the tree, not a penetration test.

## Development and testing

`setup_composer.md` local steps: PHP 8.4, `composer install`, copy `env` to `.env`. Production steps add global Composer install, `composer install --no-dev --optimize-autoloader` in `/var/www/indawo`, create `.env`, `chmod 640`, and the ODBC DSN.

No PHPUnit configuration, no test plan, and no automated test files were found. Verification in history is commit messages ("Fix PDF's", "Fix errors on Single KPI filter") rather than a suite.

`NewKnowledgeBase.md` repeats the February 2026 PHP 8.4 lessons: quote array keys, initialise nested Nemo objects, prefer `$_SESSION["ONLINE"]` with `USER` as a legacy fallback, and use `blnSaveEmail` rather than `blnInsert` for `NemoEmail`.

## Relationship to other Indawo repositories

| Repository | What this pass established |
|---|---|
| `indawo_65` (this tree) | Branch `INDAWO_84_2026` at `08157c5`, last commit 2026-03-24 |
| `indawo_84` | Separate Git repository. Silicon Ark set dated 2026-10-12. Same composer name, product name, and Sonar key. Extra realtime queue, money impact, and ODBC dashboard |
| `indawo_docker`, `indawo_online_docker` | Directories exist in Silicon Ark. They were not compared file-by-file with this tree |

Whether production `online.indawo.biz` runs this commit or a later `indawo_84` commit is **Unknown / Requires Confirmation**.
