# KPOD Digitisation Monitor — Backend

Laravel 12 JSON API backing the KPOD React SPA. All routes live under `/api/v1`.
Auth is Sanctum bearer tokens; roles and capabilities are spatie/laravel-permission.

**The code lives on the `master` branch.** `main` contains only a README — if the
working tree looks empty, you are on the wrong branch.

---

## Daily workflow

### Starting the backend

**Step 1 — the database.** This project talks to the MariaDB 10.11 instance that
ships with MacPracticeServer. It is not a Homebrew service and does not start on
login, so it has to be running before anything else works.

The easy way: **launch the MacPractice app**, which starts its own database.

The manual way, if MacPractice is not open:

```bash
sudo hdiutil attach /Library/MacPracticeServer/MacPracticeData.sparsebundle -nobrowse
sudo /Library/MacPracticeServer/MariaDB/bin/mysqld_safe \
  --datadir=/Volumes/MacPracticeData/mysqldata \
  --socket=/tmp/mysql.sock --port=3306 &
```

The first command asks for the sparsebundle password, the second for your macOS
password. Confirm it came up:

```bash
pgrep -fl mariadbd          # should print a mariadbd process
php artisan migrate:status  # should list migrations, not throw
```

**Step 2 — the API server.**

```bash
php artisan serve           # http://localhost:8000, API at /api/v1
```

Or, to run the server, queue worker and Vite together:

```bash
composer dev
```

Health check: `curl http://localhost:8000/up`.

### Stopping the backend

Stop the API server with `Ctrl+C` in its terminal. If it was backgrounded:

```bash
pkill -f "artisan serve"
```

**You normally do not stop MariaDB** — leave it running for the day. If you must:

```bash
sudo pkill -f "mysqld_safe --datadir=/Volumes/MacPracticeData"
sudo pkill -TERM -f "mariadbd --basedir=/Library/MacPracticeServer"
sudo hdiutil detach /Volumes/MacPracticeData
```

Kill the `mysqld_safe` wrapper *before* `mariadbd`, or it will restart the daemon.
Use `-TERM`, never `-9` — SIGTERM is a clean InnoDB shutdown.

Only do this when MacPractice is not in use. The same server holds the practice
data, and stopping it takes that application offline too.

---

## Common commands

| Task | Command |
| --- | --- |
| Run migrations | `php artisan migrate` |
| Reseed demo data | `php artisan db:seed` |
| Rebuild from scratch | `php artisan migrate:fresh --seed` |
| Run tests | `composer test` |
| Format code | `./vendor/bin/pint` |
| Tail logs | `php artisan pail` |
| Clear config cache | `php artisan config:clear` |

Tests run against SQLite in memory (see `phpunit.xml`), so **the test suite does
not need MariaDB running**. It is the fastest way to check work when the database
is unavailable.

---

## Configuration gotchas

**`DB_HOST` must be `localhost`, not `127.0.0.1`.** MariaDB treats `root@localhost`
(unix socket) and `root@127.0.0.1` (TCP) as different accounts with different
grants. Only the socket account accepts our password. `.env.example` ships
`127.0.0.1`, which fails with `SQLSTATE[HY000] [1045] Access denied` — this is the
single most likely cause of a connection error here.

**This app shares a database server with MacPractice.** Our schema is
`kpod_backend`; the practice data is in other schemas on the same instance. Before
running `migrate:fresh` — which drops every table in the configured database —
check that `DB_DATABASE=kpod_backend`.

**`KPOD_TODAY` is pinned.** `config/kpod.php` defaults programme "today" to
`2026-07-20` so seeded timelines are reproducible. Dashboards and planned-vs-actual
calculations read it, so they will not track the real clock until it is set to null.

**Mail goes to the log.** `MAIL_MAILER=log`, so password-reset links and new-account
credentials land in `storage/logs/laravel.log` rather than an inbox.

**Demo account passwords come from `KPOD_DEMO_PASSWORD` in `.env`**, not from a
hardcoded value. `UserSeeder` creates `super@`, `admin@` and `viewer@kpod.iq` with
it, and re-running the seeder resets them. Read the value from `.env`; do not commit
it here.

**CORS is currently wide open.** `config/cors.php` sets `allowed_origins => ['*']`.
Known issue — scope it to `FRONTEND_URL` before any deployment.

---

## Architecture

Requests flow: route → FormRequest (validation) → thin controller → service → model.

- `app/Http/Controllers/V1/` — controllers hold no business logic
- `app/Services/` — all domain logic; failures throw `ServiceException`
- `app/Http/Requests/` — validation, one class per endpoint
- `app/Http/Resources/` — response shaping
- `app/Enums/` — domain vocabulary shared with the frontend (`RoleEnum`,
  `PermissionEnum`, `StatusEnum`, `MetricEnum`, …)

Every response, success or failure, is rendered into a single envelope
(`{status, message, data, errors}`) by `App\Utils\ApiResponse` plus the exception
handlers in `bootstrap/app.php`. Do not return bare JSON from a controller.

Sites are addressed by their code (`NOC-STN`), tasks by their key (`dailydoc`).
Codes are unique only *within* a programme, so lookups go through `ResolvesProgram`
rather than route-model binding.

Every write is gated by a capability string in `PermissionEnum` that matches the
frontend's `utils/permissions.ts` exactly, so a control hidden client-side is also
refused server-side. When adding an endpoint, gate it with the same string the UI
checks.

User-facing strings live in `lang/en/messages.php` and `lang/ar/messages.php` —
both must be updated together.

Further detail: `docs/API.md`, `docs/BACKEND_PLAN.md`, `docs/FRONTEND_INTEGRATION.md`.

---

## Production server (cPanel / GoDaddy)

The live app is `icstech` on GoDaddy shared cPanel hosting
(`be.ics-technologies-irq.com`), Laravel deployed straight into
`~/public_html` (the document root *is* the app root here — not the usual
`public/`-only split).

**Deploys are push-triggered, not git-based on the server.** `.github/workflows/deploy.yml`
runs on every push to `master`: it does a fresh `composer install` on the
Actions runner, then `rsync`s the whole tree (excluding `.env`, `.git`,
`public/storage`) onto the server over SSH using a password stored in GitHub
secrets. **There is no `.git` directory on the server** — never try `git pull`
there, it will fail with "not a git repository". `deploy-icstech.yml` is the
same thing as a manual `workflow_dispatch` (a different target, "Saudi" —
not this app).

Crucially, **the workflow only copies files.** It does not run
`php artisan migrate`, does not clear caches, and does not restart anything.
Every migration still needs a human (or Claude) to SSH in and run it by hand
after the push lands.

### Deploy checklist — dynamic per what actually changed, not a fixed script

Don't run the same steps for every push — read the diff and pick the branch
that matches:

| What changed | What to do on the server |
| --- | --- |
| **Frontend only** (`KPOD-FrontEnd` repo, no backend files touched) | Nothing. Its own `deploy-icstech.yml` builds and rsyncs `dist/` on push to `main` — no SSH needed at all. |
| **Backend, no new migration** (controller/service/route/config logic) | Just confirm the rsync landed (step 2 below), then a quick functional check of what changed. `config:clear`/`route:clear` are **no-ops on this server** — there is nothing under `bootstrap/cache/` to clear (`config:cache`/`route:cache` are never run here), confirmed by inspecting that directory directly rather than assuming. |
| **Backend, new migration file** | Full sequence below, backup included — every time, regardless of how small the migration looks. |

Full sequence, when a migration is involved:

1. `git push origin master`.
2. Poll the server until the push has landed (see "After a push reaches
   `master`" below) — never run a later step against stale code.
3. **Back up the database first** (see the `mysqldump` command below), no
   exceptions — even for a migration that looks purely additive.
4. `php artisan migrate:status` on the server, to see what's actually
   pending before running anything.
5. `php artisan migrate --force`.
6. Verify the change landed (e.g. `php artisan tinker` against the specific
   row/behaviour the change touched) — don't just trust a clean migration
   output.

### Connecting

SSH is on port 22, user `icstech`, key-based auth only (cPanel → Security →
SSH Access). To set this up for a new machine/session:

1. Generate a keypair locally: `ssh-keygen -t ed25519 -f ~/.ssh/godaddy_cpanel_ed25519 -N ""`.
2. In cPanel → SSH Access → Manage SSH Keys → **Import Key**, paste the
   `.pub` file's contents (leave the private key field blank — it never
   leaves this machine), then **Authorize** the newly-imported key from the
   key list (import alone leaves it unauthorized).
3. Add an alias to `~/.ssh/config`:
   ```
   Host icstech
     HostName be.ics-technologies-irq.com
     User icstech
     Port 22
     IdentityFile ~/.ssh/godaddy_cpanel_ed25519
     IdentitiesOnly yes
   ```
4. `ssh icstech 'whoami'` should print `icstech`.

### After a push reaches `master`

The Actions run typically finishes well under a minute. Since there's no
`gh` CLI in this environment, the reliable way to know the rsync landed is
to poll the server for a file the push actually changed, e.g.:

```bash
until ssh icstech 'test -f ~/public_html/path/to/a/new/file'; do sleep 10; done
```

Then, from `~/public_html` on the server:

```bash
php artisan migrate:status   # confirm what's pending before running anything
php artisan migrate --force
php artisan config:clear && php artisan route:clear && php artisan cache:clear
```

**Always back up before migrating anything on this database.** `.env`'s
`DB_USERNAME`/`DB_PASSWORD`/`DB_DATABASE` values are **single-quoted**
(`DB_PASSWORD='...'`) — a plain `cut -d= -f2-` keeps the literal quote
characters as part of the value and silently produces the wrong password.
Strip them, then dump:

```bash
DB_USER=$(grep '^DB_USERNAME=' .env | cut -d= -f2- | sed "s/^'//;s/'$//")
DB_PASS=$(grep '^DB_PASSWORD=' .env | cut -d= -f2- | sed "s/^'//;s/'$//")
DB_NAME=$(grep '^DB_DATABASE=' .env | cut -d= -f2- | sed "s/^'//;s/'$//")
MYSQL_PWD="$DB_PASS" mysqldump -u "$DB_USER" "$DB_NAME" > ~/kpod_backend_backup_$(date +%Y%m%d_%H%M%S).sql
```

Never echo `$DB_PASS`/`$DB_USER` (or any other `.env` secret) back into a
terminal transcript that ends up in a conversation or log — pipe values
straight into the command that needs them instead of printing them first.

---

## Git commits

Never add a `Co-Authored-By: Claude ...` line, a `Claude-Session:` link, or a
"🤖 Generated with Claude Code" line to commit messages or PR descriptions in
this repo. Commits should read as if written by the developer alone.
