# Preview Deployments

`src/preview/` — Docker-based preview deployments for reviewing changes in a running application.

## Overview

When a phase enters `pending_review`, reviewers can spin up the app at a specific branch in local Docker containers, accessible through a single port via a Caddy reverse proxy.

**Key design decision: No database table.** Docker IS the source of truth. Instead of persisting rows, we query Docker directly via compose project names, container labels, and `docker compose` CLI commands.

## Architecture

```
PreviewManager
  ├── ConfigReader (discover .cw-preview.yml / compose / Dockerfile)
  ├── ComposeGenerator (generate docker-compose.yml + Caddyfile)
  ├── DockerClient (thin wrapper around docker compose CLI)
  ├── HealthChecker (poll service healthcheck endpoints)
  └── PortAllocator (find next available port 9100-9200)
```

### Lifecycle

1. **Start**: discover config → allocate port → generate compose + Caddyfile → `docker compose up --build -d` → health check → emit `preview:ready`
2. **Stop**: `docker compose down --volumes --remove-orphans` → clean up `.cw-previews/<id>/` → emit `preview:stopped`
3. **List**: `docker compose ls --filter name=cw-preview` → parse container labels → reconstruct status
4. **Shutdown**: `stopAll()` called on server shutdown to prevent orphaned containers

### Docker Labels

All preview containers get `cw.*` labels for metadata retrieval:

| Label | Purpose |
|-------|---------|
| `cw.preview` | `"true"` — marker for filtering |
| `cw.initiative-id` | Initiative ID |
| `cw.phase-id` | Phase ID (optional) |
| `cw.project-id` | Project ID |
| `cw.branch` | Branch name |
| `cw.port` | Host port |
| `cw.preview-id` | Nanoid for this deployment |

### Compose Project Naming

Project names follow `cw-preview-<nanoid>` convention. This enables filtering via `docker compose ls --filter name=cw-preview`.

## Configuration

Preview configuration is discovered from the project directory in this order:

### 1. `.cw-preview.yml` (explicit CW config)

```yaml
version: 1
services:
  frontend:
    build:
      context: .
      dockerfile: packages/web/Dockerfile
    port: 3000
    route: /
    healthcheck:
      path: /
      interval: 5s
      retries: 10
    env:
      VITE_API_URL: /api

  backend:
    build:
      context: .
      dockerfile: packages/api/Dockerfile
    port: 8080
    route: /api
    healthcheck:
      path: /health
    env:
      DATABASE_URL: postgres://db:5432/app

  db:
    image: postgres:16-alpine
    port: 5432
    internal: true    # not exposed through proxy
    env:
      POSTGRES_PASSWORD: preview
```

### 2. `docker-compose.yml` / `compose.yml` (existing compose passthrough)

If found, the existing compose file is wrapped with a Caddy sidecar.

### 3. `Dockerfile` (single-service fallback)

If only a Dockerfile exists, creates a single `app` service building from `.` with port 3000.

## Reverse Proxy: Caddy

Caddy runs as a container in the same Docker network. Only Caddy publishes a port to the host. Generated Caddyfile:

```
:80 {
  handle_path /api/* {
    reverse_proxy backend:8080
  }
  handle {
    reverse_proxy frontend:3000
  }
}
```

## Module Files

| File | Purpose |
|------|---------|
| `types.ts` | PreviewConfig, PreviewStatus, labels, constants |
| `config-reader.ts` | Discovery + YAML parsing |
| `compose-generator.ts` | Docker Compose YAML + Caddyfile generation |
| `docker-client.ts` | Docker CLI wrapper (execa) |
| `health-checker.ts` | Service readiness polling |
| `port-allocator.ts` | Port 9100-9200 allocation with bind test |
| `manager.ts` | PreviewManager class (start/stop/list/status/stopAll) |
| `index.ts` | Barrel exports |

## Events

| Event | Payload |
|-------|---------|
| `preview:building` | `{previewId, initiativeId, branch, port}` |
| `preview:ready` | `{previewId, initiativeId, branch, port, url}` |
| `preview:stopped` | `{previewId, initiativeId}` |
| `preview:failed` | `{previewId, initiativeId, error}` |

## tRPC Procedures

| Procedure | Type | Input |
|-----------|------|-------|
| `startPreview` | mutation | `{initiativeId, phaseId?, projectId, branch}` |
| `stopPreview` | mutation | `{previewId}` |
| `listPreviews` | query | `{initiativeId?}` |
| `getPreviewStatus` | query | `{previewId}` |

## CLI Commands

```
cw preview start --initiative <id> --project <id> --branch <branch> [--phase <id>]
cw preview stop <previewId>
cw preview list [--initiative <id>]
cw preview status <previewId>
```

## Frontend

`PreviewPanel` component in the Review tab:
- **No preview**: "Start Preview" button
- **Building**: Spinner + "Building preview..."
- **Running**: Green dot + `http://localhost:<port>` link + Stop button
- **Failed**: Error message + Retry button

Polls `getPreviewStatus` with `refetchInterval: 3000` while active.

## Container Wiring

- `PreviewManager` instantiated in `src/container.ts` with `(projectRepository, eventBus, workspaceRoot)`
- Added to `Container` interface and `toContextDeps()`
- `GracefulShutdown` calls `previewManager.stopAll()` during shutdown
- `requirePreviewManager(ctx)` helper in `src/trpc/routers/_helpers.ts`

## Dependencies

- `js-yaml` + `@types/js-yaml` — for parsing `.cw-preview.yml`
- Docker must be installed and running on the host
