# Technical documentation — convergence_production (Convergence 2017)

Evidence date: 2026-10-18.

Source: `git@bitbucket.org:overdrive/convergence_production.git`, `master` at `a91a555` (2021-06-15, Lloyd Sherewa). Fourteen commits, 2021-06-11 through 2021-06-15, all import work. Application comments go back to 2010.

No README, `composer.json`, `package.json`, Dockerfile, `appspec.yml`, or Bitbucket pipeline file is at the repository root. Credentials in `system.php` and in `emailscry/classes/class.MailFetch.php` are not copied here.

## Architecture

Convergence is a server-rendered PHP application. Each screen is a root `*.php` file. The file includes a Nemo page class, handles `Action` from the request, and calls `$page->Display("layout.back.incl.php")` for staff screens.

```text
Request
  system.php                         environment, session, mysqli, sysSettings
    _framework/system.functions.inc.php
    _framework/_nemo.database2.cls.php
  page (ticket.php, hr.leave.application.php, …)
    includes/<entity>.cls.php        NemoList / NemoDetails
    _framework/_nemo.cls.php         session gate, menu, toolbar
  layouts/layout.back.incl.php       Gentelella-style shell
```

The layout classes `nav-md`, `left_col`, `right_col`, and `main_container` match the Gentelella admin theme. Provenance: **Inferred** from those class names and from `vendors/`. The footer text is Silicon Overdrive. Provenance: **Implemented**.

There is no router. The URL is the PHP filename. Ajax goes to `ajaxFunctions.php` (a parallel `ajaxFunctions.pj.php` is also present).

## Application structure

| Path | Role |
|---|---|
| `*.php` at the root | Page controllers |
| `includes/*.cls.php` | Entity and report classes |
| `_framework/` | Nemo framework |
| `layouts/` | Header, footer, back-office, report, FAQ, and email layouts |
| `project.functions.inc.php` | Project helpers included from `system.php` |
| `css/`, `js/`, `images/`, `build/` | Front-end assets |
| `vendors/` | Third-party browser libraries |
| `emailscry/` | Mailbox fetch |
| `edulab/training_certificates/` | Uploaded training files |
| `uploads/`, `profilepictures/` | Uploaded files |
| `PHPExcel/`, `includes/mpdf/` | Document libraries |
| `*20210315*` and `*.bak` / `*.backup` / `*bkp*` | Dated or named copies. Not selected by `system.php` |

About 1,335 PHP files are in the tree. Most of the JavaScript and CSS counts come from `vendors/`.

## Languages and frameworks

- PHP. The active database API is mysqli (`_nemo.database2.cls.php`, comment 2014-11-26). Unquoted array keys such as `$_SESSION[USER]` are used throughout, which older PHP accepts as strings.
- The unused `_nemo.database.cls.php` still calls `mysql_query` and `mysql_fetch_object`. `system.functions.inc.php` does not include it.
- No PHP version is declared. **Unknown / Requires Confirmation.**
- UI: Bootstrap-era admin theme, jQuery, DataTables, FullCalendar 3.9 (also under `includes/fullcalendar-3.9.0`), Select2, PNotify, Chart.js, echarts, Morris, Parsley.
- PDF: mPDF under `includes/mpdf/`.
- Excel: PHPExcel under `PHPExcel/`.
- No Node, .NET, or Python application code. A few vendor packages contain their own `package.json`.

## Databases

Two MySQL databases:

| Database | Role |
|---|---|
| `convergence_2017` | Application tables and `sysSettings` |
| `convergence_2017_aux` | `tblEmail` and `tblEmailTemplate` |

Connection is `mysqli_connect` in `system.php`. Charset is set to `utf8` on the primary link (2018-08-16). A failed connection prints a critical error via `print_rr` and does not stop the rest of the file from including the framework.

`NemoDatabase` (`_nemo.database2.cls.php`) reads column metadata with `SELECT * FROM table WHERE 1=0`, fills `$Fields` from the request in `SetValues`, and inserts or updates. Nullable foreign keys that arrive as `0`, and nullable unique keys that arrive blank, are stored as NULL (2018-04-19 comments). Encrypted fields are supported (2017-02-02, 2018-01-29 comments). `Save` can be told to ignore foreign-key errors (leave save passes that flag).

There is no migration directory and no `.sql` file. Schema changes appear as commented `ALTER TABLE` blocks at the top of entity classes.

### Tables named in code

System: `sysUser`, `sysSecurityGroup`, `sysSecurity`, `sysMenuLevel1`, `sysMenuLevel2`, `sysMenuSidebar`, `sysSettings`, `sysPassword`, `sysLogin`, `sysSpam`, `sysAuditLog`, `sysCommsLog`, `sysFAQ`, `sysLog`, `sysAudit_$env`.

CRM: `crmClient`, `crmClientSite`, `crmContact`, `crmCommunication`, `crmTicket`, `crmTask`, `crmTicketOwner`, `crmDepartment`, `crmDepartmentStatus`, `crmServiceBoard`, `crmTime`, `crmProduct`, `crmProductBundle`, `crmProductBundleItem`, `crmPaymentTerms`, `crmProposal`, `crmProposalGroup`, `crmProposalItem`, `crmTaskFault`, `crmTaskFix`.

Views named in queries: `vieClient`, `vieDepartmentStatus`, `dwfProposal`.

SLA: `slaTicket`, `slaTask`, `slaSchedule`.

HR: `hrStaff`, `hrStaffAux`, `hrLeaveApplication`, `hrLeaveType`, `hrPublicHoliday`, `hrRole`, `hrCoreValue`, `hrKPA`, `hrKPI`, `hrLog`, `hrPerformanceReview`, `hrReview`, `hrDevelopmentPlan`.

Education: `eduTrainingCourse`, `eduTrainingMaterial`, `eduTrainingCertificate`.

Servers: `soServer`, `soUpdate`, `soServerUpdate`.

Mail: `tblEmail`, `tblEmailTemplate`.

`sysVAT` is referenced only in a commented lookup. `tblTest` is used by `controls.cls.php`.

## Environment configuration

`system.php` picks `$DATABASE_SETTINGS` by `SERVER_NAME`.

| Server name | Comment in file | Database host behaviour |
|---|---|---|
| `debian` | DEV REPO | Host `192.168.100.6`, database `convergence_2017` |
| `lamp` | Was localhost; 2018-08-06 points it at the hosted server | Same hosted MySQL as live, database `convergence_2017` |
| `convergence.overdrive.co.za` | LIVE PROD | Hosted MySQL `sql45.jnb1.host-h.net`, database `convergence_2017`, protocol forced to HTTPS |

Aux settings exist for `lamp` and the live host (database `convergence_2017_aux`). They do not exist for `debian`. A `debian` request that calls `switchBD` on the aux settings has no aux block. Provenance: **Implemented**. Outcome: **Unknown** until that path runs.

`www.convergence.overdrive.co.za` and a null `SERVER_NAME` are rewritten to the live name.

After connect, every `sysSettings` row whose name does not start with `vie` is copied onto `$SystemSettings`.

`display_errors` is `1`. `error_reporting` is `E_ALL` without strict, notices, or warnings.

Session lifetime is 259200 seconds. Save path:

- live: `{DOCUMENT_ROOT}/SESSION/`
- `lamp`: `{DOCUMENT_ROOT}/Convergence_2017/dev/SESSION/`
- `debian`: `SESSION/`

**Secrets.** Usernames and passwords for MySQL are literals in `system.php`. A mailbox username, password, and Office 365 host are in a comment in `emailscry/classes/class.MailFetch.php`. SMS credentials are expected in `sysSettings` (`smsUsername`, `smsPassword`, `smsLive`). None of those secrets are repeated in Silicon Ark. They are in git history and should be treated as exposed.

A 2017-11-02 comment says FTP and database details were reset after a Hetzner breach. The values now in the file are whatever was committed in the 2021 import.

## Authentication

1. `login.php` uses `NemoBasic`, so it does not require an existing session. It unsets `$_SESSION[USER]` on load.
2. User lookup joins `sysUser` to `sysSecurityGroup`.
3. `NemoPassword` uses `sysPassword` (`Type`, `UserID`, `salt`, `hash`, `dtPassword`). The hash comment says `sha(salt + password)`.
4. Session fields: `ID`, `USERNAME`, `EMAIL`, `SECURITYGROUPID`, `DEPARTMENTID`.
5. `Nemo` pages redirect to `login.php` when that session object is missing.
6. Rights load from `sysSecurity`, then the current page’s `blnView` is overwritten to `1`.
7. Five bad passwords in `sysLogin` deactivate the user.

Password-reset mail is an ajax call. Reset-link expiry defaults to `PASSWORD_Reset_Interval` (`60 MINUTE` on first install).

`ticket.ops.php`, `under.construction.php`, and the cron scripts use `NemoBasic` and do not require a staff session.

`widget.php` assigns `$_SESSION[USER]->ID = -1` after constructing `Nemo`.

## APIs

There is no versioned HTTP API. Interactions are:

| Call | Shape |
|---|---|
| Staff screens | POST `Action` and field names to the same PHP file |
| Ajax | POST to `ajaxFunctions.php` (`header`, `type`, and payload). Examples: `SendResetEmail`, proposal item edits, leave, time |
| Client decision | GET `ticket.ops.php?Action=Approved` or `Declined`, with `TicketID` and `ContactID` |
| Jobs | Request the `cron.*.php` URL |
| SMS | HTTP POST to `http://www.mymobileapi.com/api5/http5.aspx` |

`ajaxFunctions.php` is large and handles many `type` values (proposals, leave, products). It is the internal ajax surface, not a public API.

## External integrations

| System | Direction | Trigger | Notes |
|---|---|---|---|
| MySQL on `sql45.jnb1.host-h.net` | Out | Every request | Primary and aux databases |
| PHP `mail()` | Out | `NemoEmail` send | Saves a row on the aux database as well |
| Office 365 mailbox | In | `cron.email.scry.php` | Filter described as To/Cc `helpdesk@overdrive.co.za` |
| MyMobileAPI | Out | `SendSMS` | Settings keys named above. URL is HTTP, not HTTPS |
| Browser font hosts | Not confirmed in the shell | Theme assets are local under `vendors/` | |

Failure behaviour:

- Database connect failure prints the mysqli error and continues far enough to include the framework.
- `mail()` result is stored on the email object. A failed send does not roll back the business row that queued it.
- SMS `fopen` failure prints the URL error and throws after `die` in `do_post_request`.
- Email Scry writes a log on connect failure (header comment; `sysLog` insert exists in `MailFetcher`).

## Background work

No queue daemon and no worker process. Jobs are PHP pages:

| Page | Intent |
|---|---|
| `cron.hourly.php` | Every few minutes in the script’s own refresh hint (`rf_rate = 5`). Ticket, task, and performance-review mail. Leave cases are commented out |
| `cron.daily.php` | Follow-ups, SLA materialisation, leave reminders, review reminders. Reschedules itself toward 08:00 in the page’s refresh math |
| `cron.daily.createticket.php` | Older SLA ticket creator (2017-06-22) |
| `cron.daily.tickettask.php` | SLA schedule to tickets |
| `cron.email.scry.php` | Mailbox import |
| `cron.fim.php` | File integrity, comment says every 2 hours. `MAX_EXECUTION_TIME` 7193 |
| `includes/cron.leave.submit.php` | Leave submit helper included by name |

Who calls them (crontab, wget, Windows task) is **Unknown**.

## Storage

- Sessions: files, path above.
- Profile pictures: `profilepictures/`, path stored on `sysUser`.
- Training uploads: `edulab/training_certificates/`.
- General uploads: `uploads/`.
- Proposal PDFs: generated by mPDF. `Proposal::getFilename` builds the download name.
- FIM compares the environment root (`env_root` on the database settings) with `sysAudit_$env`.

## Reporting and documents

`report.console.php` includes `includes/{radReport}.php` from the posted report id. Registered reports:

- `project.summary.rpt` — Project Summary (Projects). Default.
- `hr.leave.summary.rpt` — Leave Summary.
- `hr.annual.leave.cycle.rpt` — Annual Leave Cycle.
- `hr.sick.leave.cycle.rpt` — Sick Leave Cycle.
- `hr.leave.details.rpt` — Leave Details.

Actions: Run Report, Export, Email Report, Excel, PDF.

`pydg.report.php` streams an Excel download for "Paint your day green".

`predictive.order.nemo.js.chart.rpt.php` expects a pharmacy key on the session. It is leftover Nemo code, not wired from `report.console.php`.

## Security notes (from the tree, not a penetration test)

- Database and mailbox secrets are committed.
- `display_errors` is enabled.
- SQL is often concatenated, including ticket ids and status filters. `qs()` exists and is not used everywhere.
- `iniSecurity()` grants view on the current page.
- `ticket.ops.php` and the cron URLs do not require a staff session.
- `widget.php` overwrites the session user id.
- The HTTPS redirect is in `.htaccesss`. The filename has an extra `s`, so a default Apache install will not read it.
- `ErrorDocument 404` in that same file points at `/server.error.php`. That PHP file was not in the root listing.
- Password policy can be turned off with `PASSWORD_Enable_Policy`. First-install default is on. Live value is a setting.
- FIM is the in-app file-change watcher. It is not a substitute for host monitoring.

## Development and testing

- No PHPUnit, no test directory, no CI config.
- Manual pages: `test.php`, `clientTest.php`, `email.test.php`, `controls.php`.
- Named personal copies (`.pj.php`, `.russel.php`) sit next to the pages they copy.
- `email.test.php` is a send test, not an automated test.
- Debugging helpers `print_rr`, `vd`, and `die` remain in page and framework code.

## Deployment

**Unknown / Requires Confirmation.** The live document root is implied by `convergence.overdrive.co.za` and by session paths under `DOCUMENT_ROOT`. A dev path `/var/www/html/Convergence_2017/dev/` is commented in `cron.daily.php`. No CodeDeploy, Docker, or pipeline file is in this repository.

The 2017 Hetzner comment and the host `sql45.jnb1.host-h.net` are the only infrastructure clues.

## Monitoring

- `sysLogin` and `sysAuditLog` / `sysCommsLog`.
- FIM file audit tables.
- Email Scry `sysLog` writes.
- No application metrics, health endpoint, or error tracker was found.

## Discrepancies

1. Two `NemoDatabase` classes. Only `_nemo.database2.cls.php` is on the include path from `system.functions.inc.php`.
2. `.htaccesss` versus `.htaccess`.
3. `lamp` uses the live database.
4. `opportunity.php` is an older `ticket.php`. Opportunity list classes are static HTML.
5. Parallel dated libraries (`PHPExcel20210315`, `mpdf20210315`) are not what the undated include paths load.
6. Page view rights are overwritten.
7. Task-creation metadata said the last commit was `a91a555` on `master`. That matches this inspection.
