# User Stories — Maintenance

> Produced by the Reversa **Writer** (phase: generation) · doc_level: `complete`
> Generated on 2026-09-21

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

**Actor:** Operator (the Administrator, acting at deploy time).
**Owning unit:** `artisan-migrate` (`GET /artisan`).

This journey covers a single privileged, side-effecting maintenance endpoint that runs the app's database migrations over HTTP. It was **not** documented by Scout/Archaeologist — it was found during plan reconstruction (2026-09-19) and added as a unit precisely because it is a privileged, schema-mutating endpoint. It is 🟡 that this is intended as an operator workaround for an environment with no shell access. 🟢/🟡

---

### US-MAINT-1 — Apply pending database migrations over HTTP

**As an** Operator with no shell access, **I want** to trigger the app's migrations from a URL, **so that** I can apply schema changes after a deploy.

- **Given** I am an authenticated admin
- **When** I open `GET /artisan`
- **Then** the closure runs `Artisan::call('migrate', ['--force' => true])`, applies every pending migration and records it in the migrations ledger (idempotent by migration — nothing pending applies nothing), and always returns the static HTML "Migrating completed<br>" (200) 🟢

Notes / gaps:
- **Side-effecting GET behind authentication only:** a browser prefetch, crawler, link-preview, or shared URL with a live admin session can run production migrations — no CSRF, no confirmation. Reviewed with the project owner 2026-09-21 — accepted as a known risk, kept as legacy behaviour (no change scheduled). 🟢
- **Outcome not reported:** the captured `$exitCode` is discarded (`dd($exitCode)` is commented out) and there is no `try/catch`, so a non-zero exit is indistinguishable from success and a thrown migration surfaces as a raw 500 — no audit of who/when/result. Reviewed and accepted as-is, 2026-09-21. 🟢
- No transaction wrapper → partial-migration risk on failure. 🟡
- Authenticate-only, no permission/role (ADR-0009); `--force` removes the production safety prompt (required for a no-TTY web trigger). 🟡
- The closure route can't be `route:cache`'d and isn't isolated for unit testing. 🟡

Traces to: `artisan-migrate/` (`routes/web.php:31-37`)

---

> The improvement tasks recorded in the unit (move off GET / add CSRF / signed URL; add observability + real outcome reporting; add authorization) require **human sign-off** — they change legacy behaviour and are not part of documenting it. As of 2026-09-21 the project owner declined them and chose to keep the endpoint as-is. 🔴
