lronetto-main/CLAUDE.md
Leandro Ronetto c3550384ba chore(orquestrador): adiciona stack bom-vizinho ao Makefile
Makefile:
- BOMVIZINHO_DIR + alvos up/down/logs/pull/rebuild/clone
- compose em infra/docker-compose.yml e .env em infra/.env (terceiro layout
  diferente entre os repos; por isso os -f/--env-file explicitos)
- ordem do up: main -> gitea -> wedding -> finance -> bomvizinho -> claudeweb

CLAUDE.md: tabela de onde vive compose/.env por repo, container
bomvizinho_api, URL api.bomvizinho.{DOMAIN_BASE} e comandos novos.
Gotcha da secao 9 ampliado: bom-vizinho nao traz snippet de Caddy pronto e
exige PostGIS, que a imagem atual (postgres:16-alpine) nao tem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 19:02:11 -03:00

507 lines
27 KiB
Markdown

# CLAUDE.md
Notas de projeto pro Claude Code (e qualquer dev que entre depois). Resume todas as decisões tomadas ao longo das conversas. Linguagem do projeto: **PT-BR**.
---
## 1. Visão geral
App web pro casamento de **Stefanie & Leandro**. Convidados escaneiam um QR Code na mesa, abrem o site, mandam fotos/vídeos + uma mensagem. Os noivos administram tudo num painel `/admin`.
**Escopo**: single-tenant (um casamento). Multi-tenant (SaaS) foi avaliado — fica pra um pivot futuro se houver demanda real (3+ pedidos).
---
## 2. Arquitetura: um repositório por stack Docker Compose
Cada stack é um repositório git separado, com seu próprio compose isolado. Todas compartilham uma **network Docker externa** chamada `infra-net`.
```
lronetto-main/ Postgres + Redis + MinIO + pgAdmin + Caddy (+ orquestrador)
lronetto-gitea/ Gitea + Actions runner
lronetto-wedding/ App FastAPI + sidecars de backup (+ deploy CI)
lronetto-finance/ App FastAPI (Pluggy / open finance) (+ deploy CI)
lronetto-bom-vizinho/ App FastAPI + mobile (bairro/vizinhança) (+ deploy CI)
lronetto-claudeweb/ API FastAPI + SPA + workers efêmeros (+ deploy CI)
```
No disco todos ficam como **diretórios irmãos** (ex.: `C:\Users\lrone\code\lronetto-*`). O `Makefile` orquestrador (neste repo, `lronetto-main`) referencia os outros por caminho relativo — sobrescrevível via `GITEA_DIR=` / `WEDDING_DIR=` / `FINANCE_DIR=` / `BOMVIZINHO_DIR=` / `CLAUDEWEB_DIR=`.
**Cada repo põe o compose e o `.env` num lugar diferente** — daí os `-f` / `--env-file` explícitos no Makefile orquestrador em vez de um padrão único:
| Repo | compose | `.env` |
|---|---|---|
| gitea, wedding, finance | `docker-compose.yml` (raiz) | raiz |
| bom-vizinho | `infra/docker-compose.yml` | `infra/.env` |
| claudeweb | `deploy/compose.yml` | raiz |
Mais duas pegadinhas do **claudeweb**, também já tratadas no orquestrador:
- O `up` dele builda antes a imagem do worker efêmero (`make worker-image`), que **não é serviço do compose** — por isso `up-claudeweb` delega pro Makefile do próprio repo em vez de chamar `docker compose` direto.
- Declara uma segunda network external, `claudeweb-workers` (isola os workers efêmeros da `infra-net`). O alvo `network-claudeweb` a cria reusando o `network.sh` via `INFRA_NETWORK=`.
**Por que repos separados**: cada stack tem ciclo de vida, histórico e CI próprios. Subir/derrubar ou versionar uma não afeta as outras. Gitea pode ser desativado sem tocar no app.
**`lronetto-main` é o "platform layer"** que os consumidores usam. `gitea`, `wedding`, `finance`, `bom-vizinho` e `claudeweb` conectam ao Postgres/Redis/MinIO de lá pela `infra-net`, e são expostos pelo Caddy de lá.
> Histórico: nasceu como monorepo `wedding-app` (branch `main`) com tudo em `infra/{main,gitea,wedding_photo}/`. Foi dividido em 3 repos (fresh start, sem histórico herdado — o monorepo original fica arquivado como backup).
---
## 3. Stack técnica
| Camada | Escolha | Por quê |
|---|---|---|
| HTTP framework | **FastAPI 0.115** | Async-first, OpenAPI auto, Pydantic v2 |
| ASGI server | **uvicorn** | Padrão de fato |
| ORM | **SQLAlchemy 2.0 async + asyncpg** | Tipos modernos, drivers async maduros |
| Validação | **Pydantic v2** | Built-in no FastAPI; camelCase no wire |
| Env loader | **pydantic-settings** | Falha rápido em var faltando |
| Storage SDK | **boto3** wrapped em `asyncio.to_thread` | S3 compatível, lida com MinIO + R2 + B2 idem |
| Auth | **pyjwt** + cookie HttpOnly | 7 dias, HS256, constant-time compare |
| QR codes | **qrcode** + **reportlab** | PNG/SVG/PDF A6 imprimível |
| HEIC | **pillow-heif** | Decode iPhone HEIC → JPEG no `/confirm` |
| Frontend | **Vite + React 18 + Tailwind + react-router** | Stack comum, rápida |
| DB | **Postgres 16** | Multi-banco em uma instância (wedding + gitea) |
| Cache | **Redis 7** | Gitea usa hoje (cache + sessões); wedding pode usar depois |
| Storage | **MinIO** | S3 compat self-hosted; código portável pra R2/B2 |
| Reverse proxy | **Caddy 2** | TLS auto (Let's Encrypt em prod, interno em `*.localhost` dev) |
| Git hosting | **Gitea 1.22** + Actions runner | Self-hosted, leve, compatível com GitHub Actions |
| Backup | `prodrigestivill/postgres-backup-local` + `alpine + mc` | Sidecars com cron, rotação dias/semanas/meses |
| Package mgmt | **uv** (Python), **pnpm** (Node) | Mais rápidos que pip/npm |
---
## 4. Estrutura de diretórios completa
Três repos irmãos no mesmo diretório pai:
```
lronetto-main/ # PLATFORM LAYER + ORQUESTRADOR (este repo)
├── .gitignore
├── Makefile # Orquestra as stacks (refs ../lronetto-{gitea,wedding,finance,claudeweb})
├── network.sh # Cria infra-net (idempotente)
├── CLAUDE.md # Este arquivo
├── docker-compose.yml
├── .env.example
├── caddy/Caddyfile # hostname-based routing
├── postgres/init/01-create-databases.sh # cria DBs wedding + gitea
├── minio/{init.sh, cors.json} # cria bucket wedding-media
└── pgadmin/servers.json # postgres pré-conectado
lronetto-gitea/ # GIT + CI
├── .gitignore
├── docker-compose.yml
├── .env.example
└── runner/Dockerfile # act_runner + docker-cli
lronetto-wedding/ # APLICAÇÃO (fotos do casamento)
├── .gitignore
├── docker-compose.yml # app + postgres-backup + media-backup
├── .env.example
├── Dockerfile # multi-stage Node(build web) + Python(runtime)
├── Makefile # dev local (uv, pnpm) + ops da stack + deploy
├── .gitea/workflows/deploy.yml # CI: push na main -> SSH no host -> make deploy
├── pyproject.toml, package.json, pnpm-workspace.yaml, ...
├── apps/
│ ├── api/ # FastAPI Python
│ │ ├── pyproject.toml
│ │ └── app/
│ │ ├── main.py # FastAPI app, lifespan, SPA fallback
│ │ ├── config.py # pydantic-settings
│ │ ├── db/{base,models,migrate}.py
│ │ ├── migrations/ # SQL puro, runner idempotente
│ │ ├── lib/ # auth, storage, qrcode, pdf, ids, transcode
│ │ ├── schemas/api.py # Pydantic v2 wire models
│ │ └── routes/{public,uploads,admin}.py
│ └── web/ # Vite + React + Tailwind SPA
│ ├── index.html
│ ├── vite.config.ts
│ ├── tailwind.config.ts
│ └── src/
│ ├── main.tsx, App.tsx
│ ├── routes/{Home,Upload,Gallery}.tsx + admin/{Login,Dashboard}.tsx
│ └── lib/{api,upload}.ts
├── packages/shared/ # Zod schemas TS (usado pelo web)
├── infra/backup/ # entrypoint + script do media-backup
└── backups/ # destino dos sidecars (gitignored)
├── postgres/
└── media/
lronetto-finance/ # APLICAÇÃO (open finance via Pluggy)
├── docker-compose.yml # serviço único: finance_app
├── Dockerfile, Makefile, .env.example
├── .gitea/workflows/ # CI: build + push da imagem, deploy via SSH
├── apps/{api,web}/ # FastAPI + Vite/React
└── infra-snippets/ # trechos a colar no lronetto-main (Caddy + init do postgres)
lronetto-bom-vizinho/ # APLICAÇÃO (bairro / vizinhança)
├── infra/
│ ├── docker-compose.yml # stack "plataforma" (api, plugada na infra-net)
│ ├── docker-compose.dev.yml # stack dev standalone (pg+redis+minio próprios)
│ └── .env.example # o .env vive AQUI, não na raiz
├── Makefile # up/down/deploy + up-dev standalone
├── backend/ # FastAPI + alembic (migrations)
├── mobile/ # app mobile
└── docs/DEPLOY.md # runbook de deploy e pré-requisitos da plataforma
lronetto-claudeweb/ # APLICAÇÃO (Claude Code via web)
├── deploy/
│ ├── compose.yml # api + web + worker-proxy (.env fica na RAIZ)
│ ├── Caddyfile.snippet # bloco code.lronetto.com p/ o Caddy do main
│ └── proxy/ # tinyproxy: egress dos workers
├── backend/ # FastAPI (orquestra os workers via docker.sock)
├── frontend/ # Vite/React SPA (servida por nginx)
├── worker/ # imagem efêmera, fora do compose (make worker-image)
└── migrations/ # SQL puro
```
---
## 5. Network e roteamento
### Networks
`infra-net` é **external**. Criada pelo `network.sh` (chamada por `make network`). Containers de stacks diferentes se enxergam por nome via DNS interno do Docker.
`claudeweb-workers` é uma segunda network external, usada só pelo claudeweb: os workers efêmeros sobem **apenas** nela, isolados da `infra-net` (egress via `worker-proxy`). Criada por `make network-claudeweb`. Os containers da stack main que os workers precisam alcançar (`redis`, `gitea`, `minio`) têm que ser **conectados manualmente** a ela — ver README do claudeweb:
```bash
docker network connect claudeweb-workers redis
docker network connect claudeweb-workers gitea
docker network connect claudeweb-workers minio
```
### Container names fixos
- `postgres`, `redis`, `minio`, `pgadmin`, `caddy` (stack main)
- `gitea`, `gitea_runner` (stack gitea)
- `wedding_app`, `wedding_pg_backup`, `wedding_media_backup` (stack wedding)
- `finance_app` (stack finance)
- `bomvizinho_api` (stack bom-vizinho)
- `claudeweb-api`, `claudeweb-web`, `claudeweb-worker-proxy` (stack claudeweb) — os workers efêmeros sobem e morrem com nome próprio por sessão
### URLs (com `DOMAIN_BASE`)
| Hostname | Serve |
|---|---|
| `https://wedding.{DOMAIN_BASE}` | Site dos noivos (uploads + galeria + admin) |
| `https://finance.{DOMAIN_BASE}` | App de finanças (Pluggy) — rota Caddy pendente, ver §9 |
| `https://api.bomvizinho.{DOMAIN_BASE}` | API do bom-vizinho — rota Caddy pendente, ver §9 |
| `https://code.lronetto.com` | claudeweb (API + SPA) — rota Caddy pendente, ver §9 |
| `https://gitea.{DOMAIN_BASE}` | Git hosting + Actions |
| `https://pgadmin.{DOMAIN_BASE}` | Web UI dos bancos |
| `https://minio.{DOMAIN_BASE}` | Console admin do MinIO |
| `https://media.{DOMAIN_BASE}` | S3 API pública do MinIO (uploads/downloads) |
| `ssh://git@gitea.{DOMAIN_BASE}:2222` | Git via SSH |
### `DOMAIN_BASE`
- **Dev local**: `localhost` → Caddy emite cert interno automático pra `*.localhost`
- **Produção**: domínio real (ex.: `lronetto.com`) → Let's Encrypt automático
- Mudar `DOMAIN_BASE` requer **down + up** de todas as stacks (envs lidos no boot)
### Hairpin / `extra_hosts`
`wedding_app` tem `extra_hosts: media.{DOMAIN_BASE}:host-gateway` e `wedding.{DOMAIN_BASE}:host-gateway` pra que, mesmo dentro do container, ele resolva esses hostnames pro Docker host gateway → Caddy. Assim assinaturas S3 fecham (signing host == host que o browser usa pra PUT).
---
## 6. Comandos
### Orquestrador (`lronetto-main/Makefile`)
Sobe/derruba todas as stacks de uma vez. Precisa que os outros repos estejam
como diretórios irmãos (ou ajuste `GITEA_DIR=` / `WEDDING_DIR=` /
`FINANCE_DIR=` / `CLAUDEWEB_DIR=`).
```bash
make help # lista tudo
make up # network + main + gitea + wedding + finance + bomvizinho + claudeweb
make down # inverso
make restart # down + up
make status # ps de todas as stacks
make up-main # só infra base (este repo)
make up-gitea
make up-wedding # rebuilda imagem do app
make up-finance # rebuilda imagem do app
make up-bomvizinho # rebuilda imagem do app (compose e .env em infra/)
make up-claudeweb # delega pro Makefile do repo (builda o worker-image antes)
make rebuild-{wedding,finance,bomvizinho,claudeweb} # build --no-cache + up
make down-{main,gitea,wedding,finance,bomvizinho,claudeweb}
make logs-{main,gitea,wedding,finance,bomvizinho,claudeweb}
make pull-{main,gitea,wedding,finance,bomvizinho,claudeweb}
make network # cria infra-net
make network-claudeweb # cria claudeweb-workers
make clone-claudeweb # clona o repo do claudeweb como irmão
make clone-bomvizinho # idem pro bom-vizinho
```
### App wedding (`lronetto-wedding/Makefile`)
Repo self-contained — roda standalone, sem o orquestrador.
```bash
# Dev local (fora do Docker)
make install # pnpm install + uv sync
make dev-web # vite na 5173
make dev-api # uvicorn --reload na 3000
make migrate # roda migrations
make build # build do front
make typecheck / lint
# Stack Docker (requer infra-net + platform layer de pé)
make up # up -d --build
make down / rebuild / logs / ps
make deploy # git reset --hard + up (chamado pelo CI)
```
---
## 7. Convenções
### Variáveis de ambiente
- **SCREAMING_SNAKE_CASE**
- Cada stack tem seu `.env` (cópia do `.env.example` ao lado)
- Variáveis compartilhadas entre stacks (`DOMAIN_BASE`, `MINIO_ROOT_*`, `WEDDING_DB_*`) precisam casar manualmente
- Senhas defaults nos `.env.example` são placeholders `troque-essa-senha-forte` — sempre trocar
- `SESSION_SECRET` mínimo 16 chars (sugestão: UUID + sufixo)
### IDs
- Nanoid 16 chars + prefixo: `up_xxxx...` (upload), `au_xxxx...` (audit)
- Implementação em `apps/api/app/lib/ids.py`
### API wire format
- **camelCase** (`coupleNames`, `createdAt`, `maxFileMb`, etc.)
- Mesmo schema entre Python (Pydantic) e TS (Zod) — campos batem nome-a-nome
- Erros: `{"error": "code", "details": ...}` com HTTP status apropriado
### Timestamps
- `bigint` em ms desde epoch (não `timestamptz`)
- Razões: lida fácil com JS `Date.now()`, sort sem timezone, não precisa de cast
- Default no DB: `(EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT`
### Migrations
- SQL puro em `apps/api/app/migrations/NNNN_descricao.sql`
- Runner em `app/db/migrate.py`: idempotente, valida SHA256 (impede editar migration aplicada)
- Roda automaticamente no boot do `wedding_app` (`AUTO_MIGRATE=true`)
- Schema sempre em **Postgres puro** (não SQLite syntax)
### Storage keys
- Formato: `uploads/{YYYY}/{MM}/{id}.{ext}`
- Ex.: `uploads/2026/06/up_abc123def456.jpg`
- Extensão vem do filename, fallback no MIME type
---
## 8. Fluxos importantes
### Upload (convidado)
1. `POST /api/uploads/init` → backend valida (tamanho, vídeo permitido, duração), cria row `pending`, retorna URL pré-assinada
2. Browser faz **`PUT` direto no MinIO** (não passa pelo backend) com a URL pré-assinada
3. Decisão **single vs multipart**:
- `<= 50 MB`: single PUT
- `> 50 MB`: multipart 10 MB chunks
4. `POST /api/uploads/:id/confirm` → backend faz HEAD pra verificar o objeto, completa multipart se aplicável, **se for HEIC**: transcoda pra JPEG e substitui o storage_key, marca `approved` (ou `pending` se moderation=`pre`)
5. Galeria pública lista só `status=approved`
### HEIC transcoding
- Detecção: `mime_type in {"image/heic", "image/heif"}`
- Em `/confirm` após HEAD: baixa, decoda com pillow-heif, aplica EXIF rotation, salva JPEG quality 88 progressive, escreve com `.jpg`, deleta HEIC
- **Falha não bloqueia o upload**: log + mantém HEIC original (gallery mostra placeholder)
- Razão: browsers (especialmente Android) não renderizam HEIC nativamente
### Multipart upload (cliente)
- Em `apps/web/src/lib/upload.ts`
- Sequential (não paralelo pra MVP) com `XMLHttpRequest` (precisa de progress event)
- ETag de cada chunk via header `etag` na resposta — exige CORS `ExposeHeaders: ["ETag"]` no bucket
### Admin login
- `POST /api/admin/login` body `{email, password}`
- Valida email contra `ALLOWED_ADMIN_EMAILS` (separados por vírgula no env)
- Compara senha com `ADMIN_PASSWORD` via `hmac.compare_digest` (constant-time)
- Issue cookie HttpOnly JWT HS256 com email + exp 7 dias
- `GET /api/admin/*` decora com `Depends(get_admin_email)` que verifica o cookie
### Admin: gestão de uploads
- `GET /api/admin/uploads?status=...&kind=photo|video&q=...&cursor=...` — paginação por timestamp DESC
- `PATCH /api/admin/uploads/:id` — edita `authorName` + `message`
- `POST /api/admin/uploads/:id/{approve,reject,cover}` — actions individuais
- `DELETE /api/admin/uploads/:id` — apaga do banco + storage (incl. thumbnail)
- `POST /api/admin/uploads/bulk` — `{action: approve|reject|delete, ids: [...]}` até 200 IDs
- `DELETE /api/admin/event/cover` — remove a foto de capa
### Backup
- **Postgres**: `prodrigestivill/postgres-backup-local` daily, retenção dias/semanas/meses, escreve em `./backups/postgres/` (bind mount do host)
- **MinIO**: alpine + mc cron-driven, `mc mirror` (incremental) pra `./backups/media/`
- **Backup remoto opcional**: configurar `BACKUP_REMOTE_*` no `.env` da wedding → espelha pra outro endpoint S3 (R2/B2/etc.)
### Gitea bootstrap (1ª vez)
1. `make up-main` (precisa estar de pé pro postgres + redis)
2. `make up-gitea` (runner falha porque ainda não tem token, ok)
3. Cria user admin via CLI (nome **NÃO pode ser `admin`** — é reservado):
```bash
read -s PW
docker exec -u git -it gitea gitea admin user create \
--username lronetto --password "$PW" --email lronetto@gmail.com --admin
```
4. Abre `https://gitea.{DOMAIN_BASE}` → loga
5. Avatar → **Site Administration** → **Actions** → **Runners** → **Create new Runner** → copia token
6. Cola no `.env` do repo `lronetto-gitea`: `GITEA_RUNNER_TOKEN=<token>`
7. `make up-gitea` (runner agora se registra)
### Trocar senha do Gitea
```bash
read -s NEWPW
docker exec -u git -it gitea gitea admin user change-password \
--username lronetto --password "$NEWPW"
```
### Deploy automático (CI/CD)
Vive no repo **`lronetto-wedding`** (`.gitea/workflows/deploy.yml`). A cada push
na `main` — ou disparo manual (`workflow_dispatch`) — o runner conecta por
**SSH no host** e roda `make deploy` (git reset --hard + `up` com build no repo
wedding), seguido de health check em `/api/health`.
**Por que SSH e não docker direto no runner**: o `.env` (segredos do app) fica
só no host, fora do git. O host já tem o repo clonado; o deploy só atualiza o
código e sobe.
Setup (1ª vez):
1. No host, clone `lronetto-wedding` (ex.: `/opt/lronetto-wedding`) com um usuário
SSH que rode docker e tenha acesso ao repo. (Stack `main` precisa estar de pé.)
2. Gere um par de chaves SSH e adicione a **pública** no `~/.ssh/authorized_keys`
desse usuário.
3. No repo `lronetto-wedding` no Gitea → **Settings → Actions → Secrets**, crie:
- `DEPLOY_HOST` — IP/hostname do host
- `DEPLOY_USER` — usuário SSH
- `DEPLOY_SSH_KEY` — a chave **privada** (conteúdo completo)
- `DEPLOY_PATH` — caminho do repo no host (ex.: `/opt/lronetto-wedding`)
4. (Opcional) **Variables**: `DEPLOY_PORT` se o SSH não for 22.
`make deploy` (no repo wedding) aceita `BRANCH=` pra sobrescrever a branch.
---
## 9. Gotchas conhecidos
### O platform layer ainda não conhece finance, bom-vizinho nem claudeweb (pendente)
`caddy/Caddyfile` (neste repo) tem blocos só pra `gitea`, `wedding`, `pgadmin`, `minio` e `media`. E `postgres/init/01-create-databases.sh` cria role/DB só pra wedding e gitea. Falta, por app:
**finance** — snippets prontos no próprio repo:
- `../lronetto-finance/infra-snippets/caddy-finance.snippet` → `caddy/Caddyfile`
- `../lronetto-finance/infra-snippets/postgres-finance-db.snippet.sh` → `postgres/init/01-create-databases.sh`
**claudeweb**:
- `../lronetto-claudeweb/deploy/Caddyfile.snippet` → `caddy/Caddyfile`
**bom-vizinho** — não traz snippet pronto; ver `../lronetto-bom-vizinho/docs/DEPLOY.md`. Precisa de:
- bloco `api.bomvizinho.{DOMAIN_BASE}` → `bomvizinho_api:8000` no `caddy/Caddyfile`
- role + DB `bomvizinho` **com PostGIS** — e a imagem atual (`postgres:16-alpine`) **não traz PostGIS**: exige trocar por `postgis/postgis:16-x`, o que é troca de imagem com `PGDATA` já populado (fazer com backup na mão)
- bucket `bomvizinho-media` no MinIO (`minio/init.sh`)
Enquanto não forem aplicados, `make up-finance` / `make up-bomvizinho` / `make up-claudeweb` sobem os containers mas eles não ficam acessíveis por HTTPS (e falham ao conectar no banco). Lembre que o init do postgres **só roda com `PGDATA` vazio** — em banco já existente, crie role/DB manualmente (ver abaixo).
### Postgres init script só roda na 1ª vez
`postgres/init/01-create-databases.sh` (neste repo, `lronetto-main`) é executado pelo entrypoint do postgres **apenas quando `PGDATA` está vazio**. Pra "rerodar":
```bash
make down-main
sudo rm -rf postgres/data
make up-main
```
Alternativa: criar manualmente via `docker exec -i postgres psql`.
### `docker exec -it` com heredoc
`-t` aloca TTY e conflita com stdin redirecionado. Usar **`-i` só**:
```bash
docker exec -i postgres psql -U postgres <<EOF
CREATE ROLE gitea WITH LOGIN PASSWORD 'xxx';
EOF
```
### Gitea reservou nomes
`admin`, `api`, `user`, `org`, `explore`, `install`, `repo` etc. são reservados. Use outro nome (ex.: `lronetto`, `gitadmin`). O role admin vem do flag `--admin`, não do username.
### Gitea: imagem regular vs rootless
Volumes diferentes:
- `gitea/gitea:1.22` (regular): `/data`
- `gitea/gitea:1.22-rootless`: `/var/lib/gitea` + `/etc/gitea`
Usamos a **regular** com `INSTALL_LOCK=true` pra pular o wizard `/install` no primeiro boot.
### `docker exec` por padrão entra como root
Pra Gitea, isso quebra (`Gitea is not supposed to be run as root`). Sempre `docker exec -u git ...` quando for chamar binário do gitea.
### CORS no MinIO
Bucket precisa de `ExposeHeaders: ["ETag"]` pra multipart funcionar (cliente lê ETag de cada PUT). Aplicado pelo `minio/init.sh` (repo `lronetto-main`) via `mc cors set`.
### Postgres path-style URLs
`S3_FORCE_PATH_STYLE=true` é necessário pra MinIO. Sem isso, boto3 monta URL virtual-hosted (`bucket.endpoint/key`) que MinIO single-instance não suporta.
### Mudança em `.env` exige restart
Compose só lê env vars no boot do container. `make down-X && make up-X` após editar.
### TLS local em `*.localhost`
Caddy emite cert auto-assinado pelo root local. Browser pede pra aceitar (1x por subdomínio). Pra evitar prompts:
```bash
docker exec caddy cat /data/caddy/pki/authorities/local/root.crt > /tmp/caddy-root.crt
# Importa no trust store do OS/browser
```
### Configurar firewall em produção
- VPS firewall (ufw/iptables): liberar 80 e 443
- **Cloud firewall** (security group AWS/DO/Vultr): também liberar 80 e 443 — esse é separado e quase sempre é o esquecido
- Caddy ACME challenge precisa de **80 acessível externamente**, senão Let's Encrypt falha
### DNS precisa apontar antes do `up`
Pra produção, registros A pros subdomínios (`wedding`, `gitea`, `media`, `pgadmin`, `minio`) precisam estar propagados antes do Caddy tentar emitir cert. Senão ele entra em backoff e demora.
### `ACME_EMAIL` obrigatório em prod
Let's Encrypt requer email pra contato de renovação. Em dev pode ficar vazio (Caddy usa internal CA).
### Hairpin DNS no `wedding_app`
O `extra_hosts: host-gateway` pra `media.{DOMAIN_BASE}` faz o container resolver o subdomínio pro host. Sem isso, requests server-side (HEAD/DELETE/multipart complete) vão pra IP público → roteador → host → Caddy (lentidão). Com host-gateway: container → host → Caddy (rápido).
---
## 10. Histórico de iterações (sem detalhe — referência rápida)
1. **MVP Cloudflare**: Workers + D1 + R2 + Pages + Access. Funcionou mas Access não rola em `*.workers.dev`.
2. **Migração 1**: full Docker (Node/Hono + Postgres + MinIO + Caddy). Single compose, deploy num VPS.
3. **Migração 2**: backend reescrito em Python (FastAPI + SQLAlchemy + boto3 + pyjwt). Mesmo contrato de API. Frontend não mexeu.
4. **HEIC + backup**: pillow-heif no `/confirm`, 2 sidecars de backup (Postgres + MinIO mirror).
5. **Reorganização em 3 stacks**: `infra/{main,gitea,wedding_photo}` no monorepo `wedding-app`, compartilhando `infra-net`. Caddy concentrado em `main/`.
6. **Deploy CI + rename main**: workflow Gitea Actions de deploy via SSH; branch renomeada pra `main`.
7. **Split em 3 repos**: `lronetto-main` + `lronetto-gitea` + `lronetto-wedding` (fresh start). Orquestrador no main, deploy CI no wedding. Monorepo `wedding-app` arquivado.
8. **Novas apps na plataforma**: `lronetto-finance` (open finance via Pluggy), `lronetto-bom-vizinho` (bairro/vizinhança, precisa de PostGIS) e `lronetto-claudeweb` (Claude Code via web, com workers efêmeros em rede isolada). As três entraram no Makefile orquestrador; a integração delas com Caddy/Postgres/MinIO deste repo ainda não foi feita (§9).
Repos / branches:
- `lronetto-main`, `lronetto-gitea`, `lronetto-wedding`, `lronetto-finance`, `lronetto-bom-vizinho`, `lronetto-claudeweb` (atuais; default branch `main`)
- Arquivados no monorepo `wedding-app`: `claude/wedding-qrcode-photos-C0PQt` (Cloudflare original), `claude/docker-vps-migration` (1ª migração Docker Node)
---
## 11. Onde olhar pra estender
| Quero adicionar... | Olha em |
|---|---|
| Nova rota pública | `apps/api/app/routes/public.py` |
| Nova rota admin | `apps/api/app/routes/admin.py` |
| Novo campo no upload | `apps/api/app/db/models.py` + nova migration + `apps/api/app/schemas/api.py` + atualizar `routes/uploads.py` |
| Novo bucket no MinIO | `lronetto-main`: `minio/init.sh` + variável no `.env.example` |
| Outro DB no postgres | `lronetto-main`: `postgres/init/01-create-databases.sh` + role nova |
| Novo subdomínio Caddy | `lronetto-main`: `caddy/Caddyfile` + container_name correspondente |
| Nova app na rede | novo repo com `docker-compose.yml`, declarar `infra-net` como external, conectar a `postgres`/`redis`/`minio` por nome, e referenciar no Makefile orquestrador |
| Nova tela no front | `apps/web/src/routes/` + rota em `App.tsx` |
| Schema compartilhado front-back | duplica: Zod em `packages/shared/src/schemas.ts` (TS) + Pydantic em `apps/api/app/schemas/api.py` (Python). **Camelo nos dois** |
| Workflow CI no Gitea | `.gitea/workflows/*.yml` no repo alvo (sintaxe GitHub Actions) — runner já registrado. Ex.: deploy do app em `lronetto-wedding` |
---
## 12. Decisões deferidas (a fazer se necessário)
- **Thumbnails server-side**: galeria carrega imagens full-size. Pra otimizar: `sharp` no upload `/confirm` (ou um job async), salvar `thumbnail_key`. ~2h.
- **Export ZIP do admin**: streamed ZIP de todos os uploads. ~1h.
- **Rate limit em `/uploads/init`**: hoje sem limite. Vale colocar Redis-based se houver suspeita de abuso. ~1h.
- **Email aos noivos quando upload chegar**: Resend ou SMTP. ~2h.
- **Slideshow pra projetar na recepção**: tela `/slideshow` com auto-advance. ~30 min.
- **Custom domain do bucket público** (em vez de `media.X`): mais "branded". DNS + CNAME pro MinIO. ~15 min.
- **Pivot multi-tenant SaaS**: ver seção 1 — 2-4 semanas se valer a pena.