# G014 — Live Preview Engine Walkthrough

## Summary

The Live Preview Engine allows an authenticated Couple to experience their invitation
exactly as a guest would, including while still in **Draft Mode**, without any production
data being written.

---

## What Was Built

### Files Created

| File | Purpose |
|---|---|
| [LivePreviewController.php](file:///c:/xampp/htdocs/inveetaire/app/modules/PublicInvitation/LivePreviewController.php) | Authenticated couple-only controller. Resolves payload, renders theme, injects badge |

### Files Modified

| File | Change |
|---|---|
| [PublicInvitationService.php](file:///c:/xampp/htdocs/inveetaire/app/modules/PublicInvitation/PublicInvitationService.php) | Added `getRenderedPayloadForPreview(int $workspaceId)` |
| [PublicInvitationModel.php](file:///c:/xampp/htdocs/inveetaire/app/modules/PublicInvitation/PublicInvitationModel.php) | Added `getWorkspaceById()` and `getAllEvents()` |
| [Invitation/routes.php](file:///c:/xampp/htdocs/inveetaire/app/modules/Invitation/routes.php) | Added `GET /invitation/livepreview` route |
| [edit.php](file:///c:/xampp/htdocs/inveetaire/app/views/invitation/edit.php) | Added ✨ Live Preview button in header |

---

## Architecture

The production rendering pipeline is **100% reused** with zero modifications:

```
GET /app/couple/invitation/livepreview
    ↓ Auth + RoleMiddleware + WorkspaceMiddleware (inherits from /app/couple group)
    ↓ LivePreviewController::show()
    ↓ PublicInvitationService::getRenderedPayloadForPreview($workspaceId)
        ├── getWorkspaceById()         (no slug required — session has ID)
        ├── getInvitationByWorkspace() (no status check — draft OK)
        ├── Virtual guest injected     (id=0, full_name='Live Preview')
        ├── getAllEvents()             (all workspace events — no guest filter)
        ├── getInvitationSections()
        └── InvitationRenderer::prepareRenderData() — UNCHANGED
    ↓ ThemeService::renderInvitation($payload) — UNCHANGED
    ↓ ThemeRenderer::render()                  — UNCHANGED
    ↓ ob_start() → captures HTML → injects badge before </body>
    ↓ echo $html
```

---

## Security Guarantees

- Route lives inside `/app/couple` group → requires `AuthMiddleware` + `RoleMiddleware` 
  (couple only) + `WorkspaceMiddleware` (ownership validated).
- An unauthenticated user hitting `/app/couple/invitation/livepreview` is redirected to `/login`.
- A couple cannot see another couple's draft — `active_workspace_id` from session is
  already validated against ownership by `WorkspaceMiddleware`.
- The literal string `livepreview` is **not** a registered guest token pattern in `routes/public.php`.
  Hitting `/i/{slug}/livepreview` as a guest returns 404.
- `getRenderedPayloadForPreview()` contains zero DB write calls.

---

## Read-Only Overlay

A floating badge is injected into the rendered HTML:

```
● LIVE PREVIEW
Viewing as Guest · No data saved
```

A JS interceptor wraps:
- All `<form>` `submit` events (captured in bubble phase)
- `window.fetch` POST calls
- `XMLHttpRequest` POST calls

Any interaction that would normally POST data shows a modal:

```
🔒 Live Preview Mode
This action is disabled because you are previewing the invitation.
RSVP responses, wishes, and attendance are not recorded during Live Preview.
[Got it]
```

---

## How to Verify

1. Open `/app/couple/invitation/edit` → header now shows `[👁 Preview]` `[✨ Live Preview]` `[Draft Mode]`
2. Click **Live Preview** → opens in new tab at `/app/couple/invitation/livepreview`
3. Should render the **real theme** with actual uploaded media, music, story, events
4. A floating amber badge appears top-right: **👁 LIVE PREVIEW**
5. Click **RSVP** submit → modal appears, no redirect, no DB write
6. Open incognito → navigate to `/app/couple/invitation/livepreview` → redirected to `/login`
7. Draft invitation renders correctly (no `published` requirement)
