# HERMES

**Communications Directorate Management Platform**

A system of record for a national communications directorate: who is on air
tonight, what talking points they were given, whether they acknowledged the
assignment, what actually happened on the programme, and how coverage is
trending.

---

## What this is, and what it deliberately is not

HERMES coordinates, informs and reports. It manages **the directorate's own
officers**, **the media organisations they engage with**, and **the party's own
approved messaging**.

It is **not** a voter database. There is no voter, elector, citizen, supporter
or membership table anywhere in the schema, and there is a test that fails if
one ever appears. The only people modelled are:

- **staff users** — officers and administrators with accounts;
- **media contacts** — working contact details for named presenters,
  journalists and producers, the same information a press office keeps in a
  contact book. Each record carries a lawful basis and a review date, opening
  one is written to the audit trail, and deleting one is a genuine delete
  rather than an archive.

`monitoring_items.tone` records how a *broadcast* treated the party's position.
It is a property of an item, not a rating of a journalist, and nothing
aggregates it per person. Officer analytics are framed as workload and
coverage indicators — who is carrying too much, which region is being
neglected — not as a performance ranking.

---

## Running it locally

**Requirements:** PHP 8.1+ with `pdo_mysql`, `mbstring`, `fileinfo`, `openssl`
(and `curl` for SMS, `zip` for upload backups), plus MySQL 8 or MariaDB 10.4+.
Node is needed only to rebuild the CSS.

> **On this machine:** the `php` on `PATH` (`C:\PHP`) is a stock install with
> every extension commented out and will not boot the app. Use Laragon's:
> `C:\laragon\bin\php\php-8.3.30-Win32-vs16-x64\php.exe`

```bash
# 1. Create the database
mysql -u root -e "CREATE DATABASE hermes CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"

# 2. Point the app at it (optional — the defaults assume root@127.0.0.1)
cp config/config.example.php config/local.php   # then edit

# 3. Build the schema and seed the reference data
php database/migrate.php
php database/seed.php                            # prints the founding password ONCE

# 4. Optional: a realistic directorate to look at
php database/demo.php

# 5. Serve it
php -S 127.0.0.1:8090 -t public server.php
```

Then open <http://127.0.0.1:8090>.

### Demo accounts

`php database/demo.php` creates a plausible directorate — officers across all
sixteen regions, thirty stations, seventy-five programmes and a few months of
appearances. Every demo account uses the password `Demo!Directorate2026`.

| Account | Role | Shows you |
|---|---|---|
| `coord.accra` | Regional Coordinator | **No 2FA — quickest way in.** Greater Accra only |
| `officer.demo` | Communication Officer | Only their own records |
| `analyst.demo` | Analyst | National, read-only |
| `rcd.ash` | Regional Communications Director | Ashanti only |
| `ncd.demo` | National Communications Director | Everything (needs 2FA) |
| `admin.demo` | Directorate Administrator | Runs accounts, **cannot approve messaging** |

Comparing `coord.accra` with `ncd.demo` on `/officers` is the fastest way to
see the scope model working: the same page, a different reach.

Leadership and administrator roles require a second factor. The demo enrols
them all against one shared key so the demo is usable — add it to any
authenticator app as a manual/setup key:

```
JBSW Y3DP EHPK 3PXP JBSW Y3DP EHPK 3PXP
```

Or print the current code:

```bash
php -r "require 'app/Core/App.php'; App\Core\App::registerAutoloader(); echo App\Core\Totp::code('JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP');"
```

Remove it all again with `php database/demo.php --wipe`.

---

## Architecture

No framework and no Composer, matching the rest of this estate. Everything —
the SMTP client, TOTP, QR generation, VAPID signing, CSV/XLSX export — is
hand-rolled in `app/Core/`.

```
public/           Apache document root. index.php + .htaccess ONLY.
app/Core/         Kernel, router, database, auth, RBAC, scope, mail, uploads…
app/Controllers/  Validate input, call a service, choose a response.
app/Models/       SQL. One per aggregate.
app/Services/     Business rules, transactions, audit writes.
app/Views/        Plain PHP templates.
config/           config.php (committed), local.php (gitignored), routes, permissions
database/         Numbered migrations, seed, demo, importers
storage/          logs, uploads, backups, cache — all outside the document root
bin/              cron.php, backup.php, prune.php
tests/            run.php — plain assertion runner, no PHPUnit
```

**Request lifecycle:** Apache rewrite → `public/index.php` → `App::boot()` →
security headers and CSP → CSRF check on every state-changing verb → route
dispatch → `Auth::requireLogin()` (the default) → `Gate::authorize()` →
Controller → Service → Model → View.

### Two ideas worth knowing before reading the code

**1. Routing is secure by default.** The permission slug is the third argument
to a route. Authentication is applied unless a route explicitly says
`Router::PUBLIC_ROUTE`. A route added carelessly is a locked door, never an
open one.

```php
$router->get('/officers', 'OfficerController@index', 'officers.view', 'officers');
$router->get('/login',    'AuthController@showLogin', Router::PUBLIC_ROUTE, 'login');
```

**2. Scope is enforced in the model, not the controller.** `Gate` answers
"may this role do this"; `Scope` answers "over which rows". Because list
pages, CSV exports, chart endpoints and print views all reach the database
through the same model methods, they all inherit the same restriction — so a
new export cannot accidentally ship an unscoped one. A regional director
cannot widen their view by editing a URL; the constraint overrides the filter.

### Roles

Nine seeded roles. **Only `system-administrator` holds permissions
implicitly** — that is the technical break-glass account for whoever runs the
server, and deliberately not the Director's role. The National Communications
Director is granted every business permission *explicitly*, so the permissions
matrix and the audit trail describe what that role can actually do rather than
collapsing it to "everything".

Separation of duties is real and tested: the Directorate Administrator runs
accounts and master data but cannot approve or publish official messaging; a
Regional Director can approve and publish **within their own region** but
restricted material needs a second approver from outside it; nobody can
approve their own document.

---

## Operations

```bash
php bin/cron.php               # every minute: drain the queue, run scheduled jobs
php bin/cron.php rollup 90     # backfill analytics after an import or an outage
php bin/cron.php vapid         # generate Web Push keys
php bin/backup.php             # nightly: database + uploads, with retention
php bin/backup.php --verify    # re-hash stored backups against what was recorded
php bin/prune.php              # dry run by default; --apply to act
php tests/run.php              # the full suite
php database/migrate.php --status
php database/import-constituencies.php   # load the EC register
```

### Rebuilding the CSS

Tailwind is compiled ahead of time and the output is committed, so production
needs no Node.

```bash
npm install
npm run css      # or: npm run watch
```

---

## Before this goes live

Three things need doing that code cannot do for you.

1. **Load the real constituency register.** The seeded list is a labelled
   starter set of 231 entries, not the Electoral Commission's 276. Replace
   `database/data/constituencies.csv` and run
   `php database/import-constituencies.php`. It only adds, and reports
   anything in the database that is missing from the file rather than
   deleting it.

2. **Configure outbound mail.** The committed default is `log` — messages are
   written to `storage/logs/mail.log` and nothing is actually sent, so a fresh
   checkout or a restored backup cannot email real officers. Set
   `mail.transport` in `config/local.php`. The diagnostics page flags it while
   it is still `log`.

3. **Work through `DEPLOY.md`.** It covers the Apache vhost, HTTPS, file
   ownership, cron entries, the restore drill, and a hardening checklist.

---

## Tests

```bash
php tests/run.php              # everything
php tests/run.php scope rbac   # named suites only
```

A plain assertion runner rather than PHPUnit, matching the no-Composer
constraint. It covers the parts where a silent regression would be expensive:

- **totp** — all five RFC 6238 reference vectors, replay refusal, drift window
- **qr** — Reed-Solomon verified by syndrome, against the published worked example
- **scope** — the list, the export and single-record access, including that a
  hostile filter narrows rather than widens
- **rbac** — separation of duties asserted role by role
- **workflow** — that a published document under revision still shows officers
  its approved copy
- **sanitiser** — stored-XSS vectors, and that Twi and Ewe characters survive
- **uploads** — extension allowlist, double extensions, size ceilings, traversal
- **csv** — formula injection (`=`, `+`, `-`, `@`) is defused
- **schema** — that no voter or membership table exists, asserted as a test

---

## Licence and provenance

Built for the National Communications Directorate. Architecture follows the
conventions of the existing estate (`STORES MGMT SYS` for structure,
`ANC APP` for migrations and row-level scoping).
