# Production Readiness Report — product-db (vehicle catalog)

Date: 2026-08-11 · Scope: `/mnt/devel/md/product-db` (Laravel 13.24 + Inertia v3.3 + React 19 + AG Grid, MySQL 8 on `192.168.200.4:3307`)
Method: env/config/log inspection, live DB queries, route/schedule enumeration, targeted secret scan, full gate runs. No deployment changes; 5 safe local fixes applied (listed in §12).

Verdict: **GO for staging** (2 WARN, 4 LOW, 2 INFO — no FAIL).

---

## 1. Build — OK

- `npm run build` → exit 0, clean (one pre-existing INFO: `ag-grid` chunk > 500 kB; not introduced here).
- `npm run types:check` (tsc --noEmit) → clean.
- `composer install --no-dev` **feasibility assessed, NOT run** (per instructions): `composer.lock` present (2026-08-10, `composer validate` OK); no runtime references to any require-dev package found in `app/`, `config/`, `routes/`, `bootstrap/` (larastan/boost/pail/pao/pest/faker/sail/mockery/collision are dev-only). `laravel/chisel` sits in `require`, so `composer run dev` keeps working under `--no-dev`. `optimize-autoloader: true` already set.
- `php artisan route:list --except-vendor` → 19 app routes, all expected catalog/auth/settings routes present; `/up` health route registered.

## 2. Environment — OK (drift noted)

- `.env` vs `.env.example` key sets: **identical** (52 keys each). Value drift: `DB_USERNAME=developer` vs example `root`; `APP_NAME` still starter default in both.
- `.env` is gitignored (line 15); **no `.git` repo exists** — nothing committed today; protection in place if VCS is initialized.
- `.env`: `APP_ENV=local`, `APP_DEBUG=true`, `APP_URL=http://localhost:8000` — dev-only, correct for local, must be overridden in staging (WARN-LOW).
- `SESSION_SECURE_COOKIE` absent from both `.env` and `.env.example` → session cookie sent over plain HTTP. Acceptable in dev; **commented placeholder added to `.env.example`** (§12-F3).
- Cache (`CACHE_STORE=database`), queue (`QUEUE_CONNECTION=database`), sessions (`SESSION_DRIVER=database`) **all on the same MySQL as catalog data** — risk note: a MySQL outage takes down app + cache + sessions + health endpoint simultaneously; `Cache::remember` (dashboard) also lives there. Recommendation: move cache/queue/sessions to Redis in staging/prod (do not change now — dev works as-is). WARN-MED.
- `.env` contains a **duplicate PCM_DB_* block (lines 29–33 and 36–40)** — same values, first wins in phpdotenv, zero functional impact. Cleanup only. INFO. (Left untouched per instructions.)

## 3. Logging — OK

- `LOG_CHANNEL=stack` → `single` (laravel.log); `LOG_LEVEL=debug` (dev). Deprecations to null.
- `storage/logs/laravel.log` (5.4 MB): **0 hits** for `DEVEL!now1` / `password` / `secret` / `token` context — no credentials or sensitive payloads logged (re-confirms security-report §11).
- Error handling: `bootstrap/app.php:26-30` renders JSON for `api/*` + `expectsJson` (Inertia requests) — standard, no detail leakage beyond Laravel defaults; `APP_DEBUG=false` in staging must be verified before handover.
- Maintenance mode: `APP_MAINTENANCE_DRIVER=file` set → `php artisan down/up` works out of the box. No custom 503 view (starter default). OK.

## 4. Error handling — OK (1 hardening fix applied)

- **Migration engine exit codes**: `ProductDbMigrateCommand` returns `FAILURE(1)`/`SUCCESS(0)`, `ProductDbVerifyCommand` returns 2 on unreachable source / 1 on verification failure — all locked by `ProductDbMigrateUnavailableTest` (runs in any environment, incl. CI). OK.
- 404 behavior: implicit model binding + `abort_unless(..., 404)` in closure routes (routes/web.php:66-90); no custom Inertia error page (`resources/js/pages` has none) — server-rendered Blade 404s, consistent with starter kit. LOW/INFO.
- Validation 422: FormRequests (`ModelsIndexRequest` etc.) → `Rule::exists`/`in` whitelists; unknown filters redirect back with errors, never 500. OK.
- **Deferred-prop failure**: `ModelsController::show` had `Inertia::defer(fn () => $this->featurePreview->for($model))` with no guard — a DB hiccup during the partial-reload request would 500 it. **Fixed** (§12-F1/F2) to catch, log a warning, return `null`; view guard changed `!== undefined` → `!= null`; type now `ModelFeaturePreview | null`. Covered by new test.

## 5. Database — OK (backup: NONE — see risk)

- `migrate:fresh` from scratch verified earlier (migration-report.md); `migrations` table has 25 rows, all 24 migrations + starter 3 runnable.
- `pcm_imports` run history: 2 rows, both `status=completed` (2026-08-11 00:26:21→26s, 00:26:30→36s), `api_version=1.11.0`, 20-table `row_counts`, verification notes (261/261 checks) — repeatable.
- Live tables confirmed: `jobs` (0 rows), `cache` (4), `sessions` (509 — actively used), `migrations` (25).
- **Backup assumption confirmed: NONE.** No automated dump of `product_db` or `pcm` anywhere; the dev MySQL on the Windows host is the only copy of the migrated catalog. Recommend (report only — no doc created): at minimum a documented `mysqldump product_db | gzip > backups/product_db-$(date).sql.gz` step before staging work, and a full `pcm` snapshot before the next destructive migration test. Risk HIGH for data-loss, mitigated only by "migration is repeatable from PCM". WARN-MED.

## 6. Queue / schedule — OK

- `php artisan schedule:list` → **"No scheduled tasks have been defined."** (routes/console.php only has `inspire`).
- No app jobs dispatched; `jobs` table exists (starter migration). `QUEUE_CONNECTION=database` — if a job is ever queued without a worker, nothing runs; irrelevant today (no jobs in code). INFO.

## 7. Assets — OK

- Vite manifest + 5 self-hosted woff2 Montserrat files in `public/build/assets` — **fonts are downloaded at BUILD time by the `bunny()` plugin and self-hosted; bunny.net is NOT a runtime dependency** (dev-mode preconnect only). This corrects the "runtime dependency" assumption in the audit brief.
- `/public/build` is gitignored; built on demand by `npm run build` (also in `composer setup` for CI). No committed build — standard Laravel approach; staging deploy must run the build step.

## 8. CI — WARN (1 fix applied)

- `.github/workflows/tests.yml`: PHP **8.5** (matches runtime; shivammathur supports it), Node 22, `composer setup` + `composer ci:check`. Pins are fine in Aug 2026.
- **Drift found**: `composer setup` copies `.env.example` → `.env` with `DB_CONNECTION=mysql` pointing at the unreachable dev host `192.168.200.4:3307`, then runs `php artisan migrate --force` → **would fail on every GitHub Actions run** before tests even start. Gated tests (CatalogRealDataTest, ProductDbMigrateCommandTest) skip cleanly on PCM unreachability, and `ProductDbMigrateUnavailableTest` passes everywhere — only the setup step was broken. **Fixed** (§12-F4): job-level `DB_CONNECTION=sqlite` + `DB_DATABASE=:memory:`.
- INFO: `composer ci:check` runs phpstan twice (`types:check` inside `@test` too) — harmless.

## 9. Health checks — OK (note)

- `/up` registered via `bootstrap/app.php:15`; returns 200 `{"status":"up"}` (JSON for API clients; HTML status page in browser).
- **DB-dependent**: no custom `config/health.php`/`withHealthChecks()`, so Laravel defaults apply — cache store + default DB connection, both MySQL. With everything on one DB this cannot distinguish "app healthy / DB down" — acceptable for staging, worth decoupling in prod. WARN-LOW.

## 10. Secrets — OK

- Targeted scan of `app/`, `config/`, `routes/`, `bootstrap/`, `server.php`, `.env.example`, `.github/` for hardcoded credential literals: **0 hits** outside `env()`.
- `PCM_DB_PASSWORD=DEVEL!now1` lives only in gitignored `.env` (dev server credential, documented in AGENTS.md); `.env.example` carries empty placeholders only; `config/database.php:89` fails closed (no fallback — prior fix confirmed present).
- Legacy prod credentials (`cockpit/configuration.php`, digitas/atlassian tokens) are outside this repo — noted once here per brief; rotation recommended.

## 11. Deployment config — OK (nothing to do)

- No deploy scripts (none expected); `.env.production` absent and gitignored — fine, staging will supply real env vars. No deployment tooling added (per brief).

## 12. Fixes applied (5, all safe & local)

| # | File | Change |
|---|------|--------|
| F1 | `app/Http/Controllers/Catalog/ModelsController.php` | Deferred `feature_preview` closure wrapped in try/catch → `Log::warning` + `null` on failure (was: unguarded, 500 on partial reload) |
| F2 | `resources/js/components/views/model-detail-view.tsx:104`, `resources/js/types/catalog.ts:420` | Guard `feature_preview !== undefined` → `!= null`; prop type `ModelFeaturePreview \| null` |
| F3 | `.env.example` | Added commented `# SESSION_SECURE_COOKIE=` with HTTPS-only note (no behavior change) |
| F4 | `.github/workflows/tests.yml` | Job env `DB_CONNECTION=sqlite` + `DB_DATABASE=:memory:` so `composer setup` migrate works in CI (was: MySQL to unreachable dev host) |
| F5 | `tests/Feature/Catalog/ModelsShowTest.php` | New test: deferred feature-preview failure degrades to `null` prop (156th test) |

## 13. Gates (exact output)

```
php artisan test --compact                          → 156 passed / 156, 2246 assertions (58.3 s)
PHPSTAN_TURBO=0 php vendor/bin/phpstan analyse --memory-limit=1G → 0 errors
npm run types:check                                 → clean
npm run build                                       → exit 0 (chunk > 500 kB warning pre-existing)
vendor/bin/pint (changed PHP files)                 → passed
```

## 14. Risk register

| Sev | Area | Risk | Status |
|-----|------|------|--------|
| MED | DB | No automated backup of `product_db`/`pcm` (single dev MySQL copy) | WARN — schedule mysqldump before staging |
| MED | Infra | Cache/queue/session/health all on the one MySQL → single point of failure | WARN — Redis in staging/prod |
| LOW | Env | `APP_DEBUG=true` / `APP_ENV=local` / `APP_URL=localhost` must be overridden in staging | WARN — check in staging handover |
| LOW | Security | `SESSION_SECURE_COOKIE` unset (HTTP session cookie) | Note + placeholder added; set true with HTTPS |
| LOW | Infra | `/up` health endpoint DB-dependent (defaults: cache+DB) | Note |
| LOW | UX | No custom Inertia 404/500 error pages (Blade fallback) | Note |
| INFO | CI | phpstan runs twice in `ci:check`; ag-grid chunk size | Note |
| INFO | Env | Duplicate PCM_DB block in `.env`; `APP_NAME=Laravel` default | Note |

## 15. Go / No-Go

**GO for staging** — 0 FAIL, 2 WARN (backup + single-DB SPOF), 4 LOW, 2 INFO. Application-level readiness is solid: tests, static analysis, and build are green; error paths (migration exit codes, 404, 422, deferred props) behave; secrets surface is clean. Staging must supply: real `APP_KEY`, `APP_DEBUG=false`, HTTPS `APP_URL`, `SESSION_SECURE_COOKIE=true`, and a documented MySQL backup cadence.
