# Deployment

How Aumni AMS ships to the production cPanel host, and the checklist run
before calling a release "live". See `docs/ARCHITECTURE.md` §21 for the
reasoning behind the layout and method summarised here; this document is the
runbook, not the rationale.

---

## 1. Pre-flight host requirements

Verify these once, when the hosting account is provisioned, and again after
any host-side change (PHP version bump, plan change):

| Requirement | How to check |
|---|---|
| PHP 8.3 available via MultiPHP Manager | cPanel → MultiPHP Manager |
| Extensions: `bcmath ctype curl dom fileinfo gd json mbstring openssl pcre pdo_mysql tokenizer xml zip` | `php -m` over SSH, or a `phpinfo()` script |
| `memory_limit` ≥ 256M, `upload_max_filesize` ≥ 16M, `post_max_size` ≥ 20M, `max_execution_time` ≥ 120 | MultiPHP INI Editor |
| SSH / Terminal access | strongly preferred over File Manager |
| Composer available (or installable to `~/bin`) | `composer --version` |
| Cron jobs allowed | required — the only scheduler this app has |
| `symlink()` enabled | needed for `storage:link` |
| `proc_open` / `exec` enabled | needed by Composer and `mysqldump` |
| MySQL 8.0 / MariaDB 10.6+ | required for generated columns and JSON columns |
| Free SSL (AutoSSL / Let's Encrypt) | required — `SESSION_SECURE_COOKIE` and HSTS assume HTTPS |

## 2. Directory layout

```
/home/aumni/
├── ams/                        ← application root (NOT web-accessible)
│   ├── app/ bootstrap/ config/ database/ resources/ routes/ storage/ vendor/
│   ├── .env                    (chmod 600)
│   └── public/                 ← contents deployed to public_html
│
└── public_html/                ← document root (or a subdomain's docroot)
    ├── index.php               (two paths edited to point at ../ams)
    ├── .htaccess
    ├── build/                  (Vite output, committed to git — Node never
    │                             runs on the server)
    ├── favicon.ico, robots.txt
    └── storage/                (symlink → /home/aumni/ams/storage/app/public)
```

`index.php`'s two path lines become:

```php
require __DIR__.'/../ams/vendor/autoload.php';
$app = require_once __DIR__.'/../ams/bootstrap/app.php';
```

**Why not put the whole app in `public_html`?** Because then
`https://host/.env`, `https://host/storage/logs/laravel.log` and
`https://host/database/` are one URL away. This layout makes source exposure
structurally impossible rather than dependent on a correct `.htaccess`.

**If the host forbids editing `index.php`'s paths**, deploy to a subdomain
whose document root is set directly to `/home/aumni/ams/public` in cPanel's
Domains → Document Root field — cleaner, and preferred when available.

## 3. Deployment method

**Preferred:** cPanel Git™ Version Control, pulling from a private repository.
**Fallback:** SSH + `git pull`.
**Last resort:** zip upload via File Manager — manual and error-prone; only
for a host with no Git integration and no SSH.

## 4. First deploy only

1. Create the MySQL database and a dedicated user (not `root`) via cPanel →
   MySQL Databases; grant that user all privileges on that one database only.
2. Clone the repository into `/home/aumni/ams`.
3. `cp .env.example .env`, fill in every value — see §5 below for the ones
   that need real thought, not just a placeholder.
4. `composer install --no-dev --optimize-autoloader`
5. `php artisan key:generate`
6. `php artisan migrate --seed --force` — seeds `SettingSeeder` (baseline
   settings) only; no demo data, no seeded login (see step 8).
7. `npm run build` **locally**, then commit `public/build` — Node never runs
   on the server.
8. `php artisan ams:create-master-user --generate` — the first account.
   Record the generated password once; it is never shown again.
9. `php artisan storage:link` — required from this phase onward: Company
   Logo (Settings → Company) is stored on the `public` disk.
10. Point `public_html` at `ams/public` per §2.
11. Verify `https://host/.env` returns 403/404 — see §7.
12. Install the single cron job — see §6.

## 5. `.env` values worth deliberate thought

| Key | Production value | Why |
|---|---|---|
| `APP_ENV` | `production` | Gates the CSP, HSTS, `migrate:fresh`/`db:wipe` refusal, and `uncompromised()` password checks — see `AppServiceProvider`. |
| `APP_DEBUG` | `false` | A debug trace exposes `.env` contents in the response body. |
| `APP_URL` | the real HTTPS origin | Used to build absolute URLs (password reset links do not exist, but signed URLs and the storage disk URL do). |
| `APP_TIMEZONE` | `Asia/Kolkata` (or the deployment's actual zone) | Now correctly wired to `config('app.timezone')` — a Phase 10 fix. The **`timezone` application setting** (Settings → System Preferences) is what actually governs `now()` at runtime; keep the two in agreement rather than relying on one silently overriding the other. |
| `SESSION_SECURE_COOKIE` | `true` | Must be `true` once HTTPS is live — cookies must never travel unencrypted. |
| `MAIL_MAILER` | `smtp` (or the host's relay) | `log` is a local/staging-only default; the warranty digest silently "sends" to a log file otherwise. |
| `QUEUE_CONNECTION` | `sync` | Never change this — there is deliberately no queue worker to run on shared hosting. |
| `CACHE_STORE` | `database` | Redis is not guaranteed on shared hosting. |

## 6. Deploy checklist (every release after the first)

```
LOCAL
 1. npm run build                                        # Vite → public/build
 2. composer install --no-dev --optimize-autoloader       # verify composer.lock
 3. composer check                                        # Pint + Larastan + PHPUnit, must be green
 4. git tag vX.Y.Z && git push --tags

SERVER
 5. php artisan down --secret=<token>                      # maintenance mode; the token bypasses it for QA
 6. git pull origin main
 7. composer install --no-dev --optimize-autoloader
 8. php artisan migrate --force                            # forward-only, see §9
 9. php artisan config:cache
10. php artisan route:cache
11. php artisan view:cache
12. php artisan event:cache
13. php artisan up
14. Smoke test: sign in → Dashboard → Assets → one asset detail →
    Reports → Settings → Audit Logs
15. Verify https://host/.env still returns 403/404
```

**Rollback:** `git checkout <previous-tag>`, re-run steps 7 and 9–12. Restore
the database from the pre-deploy backup (§ in `docs/BACKUP.md`) only if the
release included a destructive migration — normal practice here is
additive-only migrations (see `docs/ARCHITECTURE.md` §4), so this should be
rare.

## 7. Production environment verification

Run these once after the first deploy, and spot-check the first two after
every subsequent one:

- `curl -I https://host/.env` → 403 or 404, never 200.
- `curl -I https://host/storage/logs/laravel.log` → 403 or 404.
- `curl -I https://host/composer.json` → 403 or 404.
- `curl -I https://host/vendor/autoload.php` → 403 or 404 (only reachable at
  all if the app were mistakenly deployed with `public_html` as the app
  root instead of `ams/public` — see §2).
- Directory indexing is off: `curl https://host/storage/app/public/branding/`
  must not list files.
- `php artisan about` (over SSH) shows `Environment: production`,
  `Debug Mode: OFF`, `Cache Config: CACHED`, `Cache Events: CACHED`,
  `Cache Routes: CACHED`, `Cache Views: CACHED`.

## 8. Storage permissions

| Path | Mode | Owner |
|---|---|---|
| `storage/` and everything under it | `775` (dirs), `664` (files) | PHP user, group-writable if the web server runs as a different user in the same group |
| `bootstrap/cache/` | `775` | PHP user |
| `.env` | `600` | PHP user only |
| Everything else | `755` (dirs), `644` (files) | — |

If `storage/` or `bootstrap/cache/` are not writable by the PHP process, the
symptom is a 500 on the very first request (Laravel cannot write its compiled
views or cache files) — check this first if a fresh deploy 500s immediately.

## 9. The single cron job

cPanel → Cron Jobs → every minute:

```
* * * * * /usr/local/bin/php /home/aumni/ams/artisan schedule:run >> /dev/null 2>&1
```

This one line drives every scheduled task the application has — there is no
separate queue worker or supervisor process to configure:

| Command | Schedule | Purpose |
|---|---|---|
| `ams:send-warranty-digest` | daily at 08:00 | Warranty expiry email digest — see Settings → Notifications |
| `ams:prune-audit-logs` | weekly, Sunday 02:00 | Deletes routine audit entries past `audit_retention_months`; custody events (assigned/returned/renewed) are never pruned |

Confirm the cron actually fires: `php artisan schedule:list` shows both
entries with their next run time; check `storage/logs/laravel-*.log` the day
after go-live for evidence both ran.

## 10. Production readiness checklist

Run through this before calling any release "go-live", not only the first:

- [ ] `composer check` is green (Pint, Larastan, PHPUnit) on the exact commit being deployed.
- [ ] `.env` reviewed against §5 above — `APP_ENV=production`, `APP_DEBUG=false`, `SESSION_SECURE_COOKIE=true`.
- [ ] `php artisan migrate --force` ran with no errors; `php artisan migrate:status` shows nothing pending.
- [ ] All four caches built (`config`, `route`, `view`, `event`) — §7's `php artisan about` output confirms this.
- [ ] `storage:link` in place — `ls -la public/storage` on the server resolves to `storage/app/public`.
- [ ] Storage permissions match §8 — no 500 on first load.
- [ ] `.env`, `storage/`, `vendor/`, `.git/` all unreachable by URL — §7's curl checks all return 403/404.
- [ ] The cron job is installed and `schedule:list` shows both commands.
- [ ] SSL is active and forced (`URL::forceScheme('https')` runs automatically in production — confirm the certificate itself is valid and auto-renewing).
- [ ] A fresh off-host backup exists **before** this deploy — see `docs/BACKUP.md`.
- [ ] The smoke test in §6 step 14 passes: sign in, every sidebar module loads without error.
- [ ] The master account's generated password has been recorded somewhere durable (a password manager, not a chat message) and the account has signed in at least once to confirm it works.
- [ ] Application Settings (Settings → Company) reviewed and filled in with real values — company name, logo, contact details — rather than left at seeded defaults.
