# Relatórios - Fase de Envio por E-mail (deploy & infra)

> Entregue nesta fase: geração do **PDF** (Browsershot) a partir do snapshot imutável,
> armazenamento no **Google Cloud Storage**, **e-mail** com resumo HTML no corpo + PDF anexo,
> **destinatários** resolvidos em runtime, rastreio de entrega por destinatário, e modo de envio por
> projeto (**automático** vs **aprovação manual**) com envio/reenvio pela lista.

## Pipeline (3 estágios, todos leem o snapshot)

1. `GenerateReportRunJob` (fila `default`) - monta o snapshot. Se `report_config.delivery.mode == auto`,
   enfileira o estágio 2 **após o commit**.
2. `GenerateReportPdfJob` (fila **`reports`**, numprocs=1) - Browsershot → PDF → GCS. Único ponto com
   Chromium. Ao concluir (com `thenDeliver`), enfileira o estágio 3.
3. `ReportDeliveryJob` (fila `default`) - envia 1 e-mail por destinatário (idempotente), grava
   `report_deliveries`, atualiza `report_runs.delivery_status`.

Envio manual/reenvio: `POST relatorios/{project}/runs/{run}/enviar` (botão na lista) reusa o mesmo
pipeline a partir do estágio 2 (ou direto no 3 se o PDF já existe).

## Passos de infra (executar no deploy - staging e produção)

### 1. Swap (OBRIGATÓRIO - a VPS tem 0 swap hoje)
```bash
sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile
sudo mkswap /swapfile && sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
```

### 2. Node system-level + Chromium (para o Browsershot)
```bash
# Node LTS via NodeSource (não usar o node do cursor-server). >= 18 serve.
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs

# Libs de sistema do Chromium headless (Debian/Ubuntu) - ESTE é o passo que
# mais falha: sem elas o Chrome nem inicia ("error while loading shared
# libraries: libatk-1.0.so.0 ..."). Precisa de root.
sudo apt-get install -y libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
  libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libcairo2 \
  libpango-1.0-0 libasound2 libatspi2.0-0 fonts-liberation
```
- **puppeteer é dependência npm do projeto** (`package.json`), não global. O `browser.cjs` do
  Browsershot faz `require('puppeteer')`, então ele precisa estar no `node_modules`. O `npm install`
  do deploy (passo 5) já o instala e baixa o Chromium para o `PUPPETEER_CACHE_DIR`.
- **PIN OBRIGATÓRIO: `puppeteer@^22`.** A partir da v23 o puppeteer é **ESM-only** e o
  `require('puppeteer')` do Browsershot quebra com `ERR_REQUIRE_ESM`. A v22 é CommonJS e funciona.
  Não subir para v23+ sem o Browsershot passar a usar `import()`.
- Descobrir o caminho do Chrome para o `.env` (`REPORTS_CHROME_PATH`):
  ```bash
  PUPPETEER_CACHE_DIR=/var/www/.cache/puppeteer node -e "console.log(require('puppeteer').executablePath())"
  ```
  Se `REPORTS_CHROME_PATH` ficar vazio, o Browsershot usa o Chrome que a puppeteer resolve (exige o
  `PUPPETEER_CACHE_DIR` no environment do worker - passo 4).

### 3. .env (staging e produção têm valores próprios)
```dotenv
# PDF / Browsershot
REPORTS_NODE_BINARY=/usr/bin/node
REPORTS_CHROME_PATH=/var/www/.cache/puppeteer/chrome/linux-XXXX/chrome-linux64/chrome
REPORTS_PDF_DISK=gcs

# Google Cloud Storage (service account - NÃO é o OAuth de login)
GOOGLE_CLOUD_PROJECT_ID=seu-projeto
GOOGLE_CLOUD_KEY_FILE=/var/secure/gcs-signal.json      # fora do webroot, chmod 600
GOOGLE_CLOUD_STORAGE_BUCKET=signal-relatorios
GOOGLE_CLOUD_STORAGE_PATH_PREFIX=staging               # separa staging/prod no mesmo bucket
```
- Service account com papel **Storage Object Admin** no bucket. Bucket **privado** (sem acesso
  público); download no sistema é via signed URL (`temporaryUrl`, 15 min).
- Resend já configurado (Configurações › APIs). Domínio `exent.com.br` verificado.

#### Criar o bucket + service account (uma vez por projeto GCP)

Via `gcloud` (recomendado; requer `gcloud auth login` com conta que administra o projeto):
```bash
export PROJECT_ID=seu-projeto           # o mesmo do Cloud Console de login
export BUCKET=signal-relatorios         # nome global único
export SA=signal-reports                # id da service account

gcloud config set project "$PROJECT_ID"

# 1. Bucket privado, região São Paulo, uniform access + prevenção de acesso público
gcloud storage buckets create "gs://$BUCKET" \
  --location=southamerica-east1 \
  --uniform-bucket-level-access \
  --public-access-prevention

# 2. (opcional) lifecycle: apagar PDFs após 365 dias
printf '{"rule":[{"action":{"type":"Delete"},"condition":{"age":365}}]}' > /tmp/lc.json
gcloud storage buckets update "gs://$BUCKET" --lifecycle-file=/tmp/lc.json

# 3. Service account só para o Signal
gcloud iam service-accounts create "$SA" --display-name="Signal Relatórios"
export SA_EMAIL="$SA@$PROJECT_ID.iam.gserviceaccount.com"

# 4. Permissão SÓ neste bucket (não no projeto todo) - Storage Object Admin
gcloud storage buckets add-iam-policy-binding "gs://$BUCKET" \
  --member="serviceAccount:$SA_EMAIL" \
  --role="roles/storage.objectAdmin"

# 5. Gera a chave JSON (guardar FORA do webroot; nunca commitar)
sudo mkdir -p /var/secure
gcloud iam service-accounts keys create /var/secure/gcs-signal.json \
  --iam-account="$SA_EMAIL"
sudo chmod 600 /var/secure/gcs-signal.json
sudo chown www-data:www-data /var/secure/gcs-signal.json   # dono = usuário do PHP/worker
```

> Console (alternativa ao gcloud): **Cloud Storage › Buckets › Create** (Region `southamerica-east1`,
> Access control **Uniform**, **Enforce public access prevention**). Depois **IAM & Admin › Service
> Accounts › Create**, e em **Buckets › (bucket) › Permissions › Grant access** dá
> `Storage Object Admin` só para essa SA. A chave sai em **Service Accounts › (SA) › Keys › Add key ›
> JSON**.

Preencher no `.env` do ambiente:
```dotenv
GOOGLE_CLOUD_PROJECT_ID=seu-projeto
GOOGLE_CLOUD_KEY_FILE=/var/secure/gcs-signal.json
GOOGLE_CLOUD_STORAGE_BUCKET=signal-relatorios
GOOGLE_CLOUD_STORAGE_PATH_PREFIX=staging     # staging|prod - separa os dois no mesmo bucket
REPORTS_PDF_DISK=gcs
```

Teste rápido (após `php artisan config:clear`):
```bash
php artisan tinker --execute='
Storage::disk("gcs")->put("smoke.txt","ok");
echo Storage::disk("gcs")->get("smoke.txt")."\n";
echo Storage::disk("gcs")->temporaryUrl("smoke.txt", now()->addMinutes(5))."\n";
Storage::disk("gcs")->delete("smoke.txt");'
```
Deve imprimir `ok` e uma URL assinada (`https://storage.googleapis.com/...&X-Goog-Signature=...`).

### 4. Supervisor - worker isolado da fila `reports`
Adicionar em `/etc/supervisor/conf.d/exent.dashboard.conf`:
```ini
[program:exent-dashboard-reports]
command=/usr/bin/php /var/www/.../dashboard/artisan queue:work database --queue=reports --sleep=3 --tries=2 --timeout=180 --max-time=3600
directory=/var/www/.../dashboard
autostart=true
autorestart=true
numprocs=1
user=ep5_developer
environment=PUPPETEER_CACHE_DIR="/var/www/.cache/puppeteer"
stopwaitsecs=200
```
> `numprocs=1` = um Chromium por vez. `--timeout=180` < `retry_after` (720) da conexão. NÃO colocar
> a fila `reports` nos workers de sync.
```bash
sudo supervisorctl reread && sudo supervisorctl update
```

### 5. Pós-deploy (sempre)
```bash
php artisan migrate --force
php artisan config:clear   # config/reports.php + disco gcs
php artisan queue:restart  # workers seguram Provider/Job antigos em memória
npm run build
```

## Verificação end-to-end
1. `Storage::disk('gcs')->put('t.txt','ok')` no tinker → confere no bucket; apagar.
2. Projeto de teste: ciclo semanal + modo **automático** + destinatário de teste + ações do período.
3. `php artisan reports:dispatch-due --project=<id> --force-time` → snapshot gerado → PDF na fila
   `reports` → `pdf_status=done` (objeto no GCS) → envio → `report_deliveries` = sent.
4. E-mail: corpo HTML (KPIs + ações) + PDF anexo com criativos Meta embutidos.
5. Modo **manual**: gera e NÃO envia; botão "Enviar" na lista dispara. Reenvio não duplica quem já
   recebeu (`sent`).
6. Baixar PDF na lista (ícone download) → abre signed URL do GCS.

## Pontos de atenção
- **Thumbnails Meta**: bytes baixados no render do PDF (a URL do snapshot expira ~1h); placeholder em
  falha. Cap de quantidade em `config/reports.php` (`thumbnails.max_per_report`).
- **Falha de PDF/envio não é re-tentada pelo agendador** (o `reports:dispatch-due` deduplica por run
  `scheduled` do dia). Reprocessar via botão "Enviar" na lista (regenera PDF se preciso).
- **Paridade de blocos**: `ReportBlockCatalog`/`ReportPrintPresenter` (PHP) espelham
  `reportBlocks.js`/`reportPreviewRenderers.jsx` (front). Ao alterar blocos/colunas num lado, alterar
  no outro.
