# mBlog — Deployment Runbook (Modules 1–6)

An ordered, low-risk procedure to ship the enterprise re-architecture to
production. Nothing destructive runs automatically; the database migration is a
separate, deliberate step that is rehearsed on a copy first.

Supporting scripts live in `scripts/`; server config in `config/`.

---

## Final layout on the server

The document root is `web/`. The application layer sits **above** it and is not
web-accessible:

```
/var/www/mblog/                 <- PROJECT_ROOT
├── app/                        <- Core, Support, Repositories, Services, Http (Modules 2,3,6)
│   └── bootstrap.php
├── config/
│   └── app.php                 <- reads MBLOG_DB_* and ADMIN_PASSWORD_HASH
├── logs/    (writable)         <- api.log, scheduler.log
├── cache/   (writable)         <- rate-limit buckets
├── scripts/ scheduler.php
├── backups/
└── web/                        <- DocumentRoot
    ├── index.html app.js styles.css sw.js manifest.webmanifest .htaccess
    ├── post.php category.php page.php sitemap.php robots.txt
    ├── images/  (writable)
    ├── admin/   index.php  api.php
    └── api/     index.php (legacy)   v1/index.php (versioned)
```

If your hosting only exposes the docroot and you cannot place `app/` above it,
put `app/`, `config/`, `logs/`, `cache/`, `scripts/` anywhere off-web and set an
`MBLOG_APP_BASE` include path (ask before doing this — the entry points resolve
`app/` via `dirname()` today and would need a one-line tweak).

---

## Environment variables (important)

`config/app.php` reads credentials via `getenv()`. How to provide them depends
on your PHP SAPI:

- **mod_php:** the `SetEnv` lines in the vhost work (`getenv()` sees them).
- **php-fpm:** `SetEnv` is *not* reliably visible to `getenv()`. Set them in the
  FPM pool instead:
  ```
  env[MBLOG_DB_HOST] = 127.0.0.1
  env[MBLOG_DB_NAME] = mblog
  env[MBLOG_DB_USER] = mblog_app
  env[MBLOG_DB_PASS] = ...
  env[ADMIN_PASSWORD_HASH] = ...
  ```
  and reload php-fpm.
- **Last resort:** edit the fallbacks in `config/app.php` directly.

Verify from the server: `php -r 'var_dump(getenv("MBLOG_DB_NAME"));'` (run under
the same user/SAPI context where possible).

---

## Pre-flight (do once, before the window)

1. Confirm PHP and extensions: `php -v` (≥ 8.1) and
   `php -m | grep -E 'pdo_pgsql|mbstring'` — both must be present. Install
   `php-mbstring` if missing.
2. Confirm PostgreSQL reachable and the `mblog_app` role/DB exist.
3. Confirm the Apache user (e.g. `apache`) and that `AllowOverride All` is set
   for the docroot (so `.htaccess` routing works).
4. Assemble a **staging tree** = a copy of the current production tree with the
   module bundles unzipped over it, in this order:
   `mblog_api_v1` → `mblog_admin` → `mblog_seo` → `mblog_feed` → `mblog_article`.
   (Later bundles' `config/app.php`, `.htaccess`, and `post.php` supersede
   earlier ones — that is intended.) Keep the legacy `web/api/index.php`.

---

## Deploy (maintenance window)

### 1. Backup
```
PROJECT_ROOT=/var/www/mblog ./scripts/backup.sh
```
Produces `backups/db-*.sql.gz` and `backups/files-*.tar.gz`.

### 2. Rehearse the migration on a copy
```
PG_SUPERUSER=postgres ./scripts/rehearse-migration.sh backups/db-<ts>.sql.gz ./mblog_module1
```
Must end with `REHEARSAL OK`. If not, stop and investigate — do not touch prod.

### 3. Apply the migration to production
The migration is transactional, additive, idempotent and reversible.
```
psql -h 127.0.0.1 -U mblog_app mblog -v ON_ERROR_STOP=1 -f mblog_module1/001_module1_core_alignment.up.sql
psql -h 127.0.0.1 -U mblog_app mblog -f mblog_module1/001_verify.sql
```
Expect zero `mismatched_created_at`, the new tables/columns listed, and the
`schema_migrations` row present.

### 4. Set environment variables
Fill in `config/mblog.env.example` values via the vhost or FPM pool (see above),
including a real `ADMIN_PASSWORD_HASH`
(`php -r 'echo password_hash("…", PASSWORD_DEFAULT);'`).

### 5. Deploy the files
```
PROJECT_ROOT=/var/www/mblog APACHE_USER=apache ./scripts/deploy.sh /path/to/staging
```
This lints every PHP file, backs up again, syncs additively (legacy `/api`
preserved), and makes `logs/`, `cache/`, `web/images/` writable.
Then, if the vhost changed: `systemctl reload httpd`.

### 6. Disable PageSpeed caching of the API (stale-feed fix)
Ensure the vhost has the `ModPagespeedDisallow "*/api/*"` block, then flush:
```
touch /var/cache/mod_pagespeed/cache.flush   # path may vary
systemctl reload httpd
```

### 7. Install the scheduler cron
`crontab -u apache -e`, add the line from `config/mblog-scheduler.cron`.

### 8. Rotate the API key
Deactivate the test key exposed during development and issue a fresh one:
```sql
-- generate a key, then store only its hash:
--   KEY=$(openssl rand -hex 24); echo "$KEY"; printf '%s' "$KEY" | sha256sum
INSERT INTO api_client (name, key_hash, is_active) VALUES ('ingest-prod', '<sha256>', true);
UPDATE api_client SET is_active = false WHERE name <> 'ingest-prod';
```
Update any ingestion clients to send the new key via `X-API-Key`.

### 9. Health check
```
./scripts/healthcheck.sh https://mblog.in
```
Must print `HEALTHCHECK OK`. Then spot-check by hand: open `/`, a post
`/<slug>`, a category `/category/<slug>`, a static page `/page/<slug>`, and log
into `/admin/`.

### 10. Finish
Submit `https://mblog.in/sitemap.xml` in Google Search Console. Watch
`logs/api.log` and the Apache error log for a few minutes.

---

## Rollback

- **Files:** restore the tarball:
  `tar -xzf backups/files-<ts>.tar.gz -C /var/www/mblog` then reload Apache.
- **Frontend only:** because the feed/article/SEO changes are additive and the
  legacy `/api` still works, restoring the previous `web/app.js`, `post.php`,
  `.htaccess`, and `sw.js` reverts the UI without touching the database.
- **Database (last resort):** the migration is reversible via
  `001_module1_core_alignment.down.sql`, **but** rolling it back drops the new
  tables (audit_trail, media, static_pages, …) and their data. Prefer a
  forward-fix. Only run `down.sql` if the added structures are themselves the
  problem, and take a fresh backup first.

The health check exiting non-zero at step 9 is the trigger to roll back.

---

## Notes
- The public site works with **Modules 1 + 2 live**; Feed/Article/SEO depend on
  that, which is why the order above applies the migration and API before the
  frontend.
- `deploy.sh` never runs the migration and never uses `rsync --delete`, so it
  cannot remove the legacy API or user-uploaded images.
- Re-running `deploy.sh` is safe; re-applying the migration is a no-op.
