# Handoff: Prumo — Setup inicial até dashboard com logout

**Objetivo:** Partir do zero e chegar em `app.prumoboard.com` funcionando: landing page
com "Criar conta", "Entrar" e "Entrar com Google"; autenticação completa (email/senha
+ Google OAuth); redirecionamento para dashboard mínimo com nome do usuário e botão
"Sair".

> ## Como usar este arquivo (protocolo de execução)
> - Executar **uma etapa por vez**, na ordem. Cada etapa é **estanque** e tem como
>   verificar no fim.
> - Ao concluir uma etapa: marcar `[x]` (etapa e sub-itens), **preencher "Notas de
>   execução"**, salvar este arquivo e **PARAR**.
> - **Esperar o aval** do usuário **ou** a correção de erros. Erros → corrigir na
>   mesma etapa e parar de novo.
> - **No aval:** fazer **commit + push** dos arquivos da etapa (incluindo este
>   handoff e os untracked relevantes). Depois **esperar a liberação** para iniciar a
>   **próxima** etapa.
> - **Entrega é do Claude; deploy/teste costuma ser do usuário.** Onde disser
>   "Verificar", o Claude entrega e descreve o teste; se não der para rodar aqui, o
>   usuário roda e retorna o comportamento (ou os erros/logs).

---

## Decisões (já acordadas)

1. **Framework:** Flask + Jinja2 (server-side HTML; poucos endpoints retornam JSON).
2. **Auth:** email/senha via Flask-Login + Google OAuth via Authlib.
3. **Banco local:** SQLite (sem instalação). **Banco em prod:** MySQL (cPanel).
4. **Driver MySQL:** PyMySQL (Python puro, funciona em shared hosting sem extensão C).
5. **Deploy:** cPanel Passenger WSGI em `app.prumoboard.com`; auto-deploy via webhook GitHub.
6. **Monorepo:** `C:\Pr\app\prumo\` com `app/` (Flask) e `site/` (redirect .htaccess).
7. **`prumoboard.com`** → redireciona para `app.prumoboard.com` via `.htaccess` no **docroot específico do domínio** (`public_html/prumoboard.com/.htaccess`), nunca em `public_html/` (que cascatearia para bianch.in e demais sites).
8. **GitHub:** repositório privado pessoal (ricardobianchin@gmail.com); pode ser transferido para org futuramente.
9. **Rotas:** nenhuma rota diretamente em `app.py`. Tudo em Blueprints dentro de subpastas de `routes/`, organizadas por contexto.
10. **Ordem das etapas:** estrutura vazia → routes diagnóstico → deploy → banco → landing page → auth email → Google OAuth → dashboard.

---

## Estado atual (pontos de toque)

- @C:\Pr\app\prumo\ — raiz do projeto (pasta vazia, repositório ainda não criado).
- @C:\Pr\app\prumo\issues\start\start-handoff.md — este arquivo.

---

## Etapas

### [x] Etapa 1 — Estrutura de pastas e arquivos base

**Objetivo:** Criar toda a estrutura de diretórios e arquivos de configuração; `app.py` com `create_app()` sem nenhuma rota; Flask inicia sem erros.

Arquivos:
@app/passenger_wsgi.py
@app/app.py
@app/config.py
@app/requirements.txt
@app/.env.example
@app/.cpanel.yml
@site/.htaccess
@.gitignore
@README.md

- [x] Criar árvore de pastas completa
- [x] `app/app.py` — `create_app()` sem blueprints ainda
- [x] `app/config.py` — classe `Config` com `SECRET_KEY`, `DATABASE_URL`, Google OAuth keys
- [x] `app/passenger_wsgi.py` — expõe `application = create_app()`
- [x] `app/requirements.txt` — Flask, SQLAlchemy, PyMySQL, Flask-Login, Authlib, python-dotenv
- [x] `app/.env.example` — template de variáveis
- [x] `app/.cpanel.yml` — pip install após pull
- [x] `site/.htaccess` — redirect para app.prumoboard.com
- [x] `.gitignore` — .env, *.db, __pycache__/, .venv/
- [x] `README.md`
- [x] `__init__.py` vazios em routes/, routes/health/, routes/auth/, routes/main/, models/, services/
- [x] `.venv` criado com Python 3.14.5 64-bit via pyenv-win
- [x] `pip install -r requirements.txt` no .venv — OK

**Verificar:**
```
cd C:\Pr\app\prumo\app
pip install flask
flask --app app run --debug
```
Esperado: servidor sobe sem erros. `curl http://localhost:5000/` retorna HTTP 404 (correto — nenhuma rota existe ainda).

_Notas de execução:_ `.venv` com Python 3.14.5 64-bit (pyenv-win). Flask subiu; `GET /` retornou HTTP 404 conforme esperado. Todos os arquivos e pastas criados.

---

### [x] Etapa 2 — Routes de diagnóstico (health, ping, helloworld)

**Objetivo:** Blueprint `health` em `routes/health/` com três endpoints de diagnóstico; nenhuma rota em `app.py`.

Arquivos:
@app/routes/health/__init__.py
@app/routes/health/health.py
@app/app.py

- [x] `app/routes/health/health.py` — Blueprint `health` com prefixo `/health`:
  - `GET /health/` → JSON `{"status": "ok", "service": "prumo", "timestamp": "<ISO>"}`
  - `GET /health/ping` → JSON `{"pong": true}`
  - `GET /health/hello` → JSON `{"message": "Hello Prumo"}`
- [x] `app/routes/health/__init__.py` — expõe `blueprint` para importação limpa
- [x] `app/app.py` — registrar blueprint `health` (único `register_blueprint` por enquanto)

**Verificar:**
```
flask --app app run --debug
```
```
curl http://localhost:5000/health/
curl http://localhost:5000/health/ping
curl http://localhost:5000/health/hello
```
Esperado: cada um retorna JSON com HTTP 200. `curl http://localhost:5000/` ainda retorna 404 (não existe rota raiz ainda — correto).

_Notas de execução:_ `health.py` com Blueprint e 3 endpoints; `__init__.py` expõe `blueprint`; `app.py` registra via `app.register_blueprint`. Timestamp em UTC com `datetime.now(timezone.utc).isoformat()`.

---

### [x] Etapa 3 — Repositório GitHub e deploy no cPanel

**Objetivo:** Código no GitHub; cPanel puxa automaticamente a cada `git push`; `curl https://app.prumoboard.com/health/hello` retorna `{"message": "Hello Prumo"}`.

Arquivos: (nenhum arquivo novo — configuração de serviços externos)

**Passos — o usuário executa, Claude guia:**

- [x] **Local:** `git init` na raiz `C:\Pr\app\prumo\`; `git add`; `git commit -m "chore: estrutura inicial"`
- [x] **GitHub:** criar repo privado `prumo`; `git remote add origin <url>`; `git push -u origin main`
- [x] **cPanel — Subdomínio:** Domains → Create Subdomain → `app` em `prumoboard.com` → `/home/bianchin/public_html/app.prumoboard.com/`
- [x] **cPanel — Python App:** Software → Setup Python App:
  - Python 3.13
  - Application root: `/home/bianchin/public_html/app.prumoboard.com`
  - Application startup file: `passenger_wsgi.py`
  - Application Entry point: `application`
- [x] **cPanel — SSH Key:** chave gerada no cPanel adicionada ao GitHub como Deploy Key (read-only)
- [x] **cPanel — Git Version Control:** Clone URL `git@github.com:ricardobianchin/prumo.git` → `/home/bianchin/public_html/app.prumoboard.com`
- [x] **Deploy automático:** webhook do cPanel indisponível na versão instalada; GitHub Actions bloqueado por pendência de billing. Deploy via `python deploy.py` (chama `VersionControl/update` + `VersionControlDeployment/create`). Pull manual via SSH quando a API não atualiza.
- [x] **cPanel — pip install (primeira vez):** feito manualmente via terminal cPanel

**Verificar:**
1. `git push` de qualquer mudança local
2. Aguardar ~30s
3. cPanel → Git Version Control → log do último deploy (pull bem-sucedido)
4. `curl -i https://app.prumoboard.com/health/hello` → HTTP 200, `{"message": "Hello Prumo"}`
5. SE FALHAR → cPanel → Logs → Error Log

_Notas de execução:_ `passenger_wsgi.py` na raiz precisou de três correções: recursão infinita (versão antiga no servidor), `sys.path` apontando para raiz em vez de `app/` (onde está o factory), e `VersionControl/update` via API não puxava código novo (resolvido com `git pull` manual via SSH + deploy via `python deploy.py`). `curl https://app.prumoboard.com/health/hello` retorna HTTP 200 `{"message":"Hello Prumo"}`. ✓

---

### [x] Etapa 4 — Banco de dados e modelo User

**Objetivo:** SQLAlchemy configurado (SQLite local / MySQL prod); tabela `users` criada via `flask create-db`.

Arquivos:
@app/database.py
@app/models/__init__.py
@app/models/user.py
@app/app.py

- [x] `app/database.py` — `db = SQLAlchemy()`; função `init_db(app)` que chama `db.init_app(app)`
- [x] `app/models/user.py` — modelo `User`: `id`, `email` (unique, não nulo), `name`, `password_hash` (nullable), `google_id` (nullable), `created_at`
- [x] `app/app.py` — importar e chamar `init_db(app)`; adicionar comando CLI `flask create-db` que executa `db.create_all()`
- [x] `app/config.py` — `SQLALCHEMY_DATABASE_URI` (nome que Flask-SQLAlchemy lê) mapeado de `DATABASE_URL` no `.env`
- [x] `app/.env.example` — já tinha os dois exemplos de `DATABASE_URL`; sem alteração
- [x] `.gitignore` — adicionado `app/instance/prumo.db` (Flask cria o banco em `instance/`)

**Verificar:**
```
flask --app app create-db
```
Arquivo `prumo.db` criado. Confirmar:
```
flask --app app shell
>>> from models.user import User
>>> User.query.all()
[]
```

_Notas de execução:_ `database.py` criado com `db = SQLAlchemy()` e `init_db(app)`. `models/user.py` com campos `id`, `email`, `name`, `password_hash`, `google_id`, `created_at`. `app.py` chama `init_db(app)` e importa `User` dentro de `create_app()` (necessário para registrar o modelo no metadata do SQLAlchemy antes de `create_all()`). `config.py`: renomeado `DATABASE_URL` para `SQLALCHEMY_DATABASE_URI` (nome que Flask-SQLAlchemy lê). `flask create-db` rodou com sucesso; `User.query.all()` retornou `[]`. Banco gerado em `app/instance/prumo.db`.

---

### [x] Etapa 5 — Landing page visual

**Objetivo:** `GET /` exibe HTML com logo, tagline, botões "Criar conta", "Entrar" e "Entrar com Google" (formulários sem backend ainda).

Arquivos:
@app/routes/main/__init__.py
@app/routes/main/main.py
@app/templates/base.html
@app/templates/landing.html
@app/templates/auth/login.html
@app/templates/auth/register.html
@app/static/css/main.css
@app/app.py

- [x] `app/routes/main/main.py` — Blueprint `main` sem prefixo; rotas `GET /`, `GET /login`, `GET /register` renderizando os templates
- [x] `app/routes/main/__init__.py` — expõe `blueprint` para importação limpa
- [x] `app/templates/base.html` — layout base: `<head>` com charset, viewport, link CSS; bloco `content`
- [x] `app/static/css/main.css` — CSS mínimo sem framework: fonte, cores, centralizado, responsivo
- [x] `app/templates/landing.html` — estende `base.html`; nome "Prumo" + tagline; botões: "Criar conta" → `/register`, "Entrar" → `/login`, "Entrar com Google" → `/auth/google`
- [x] `app/templates/auth/login.html` — formulário email + senha (POST ainda inativo); link "Criar conta"
- [x] `app/templates/auth/register.html` — formulário nome + email + senha (POST ainda inativo)
- [x] `app/app.py` — registrar blueprint `main`

**Verificar:**
`http://localhost:5000/` no browser → landing page com 3 botões.
`/login` → formulário. `/register` → formulário. `/auth/google` → 404 (esperado).
`/health/hello` → ainda retorna JSON (diagnóstico intacto).

_Notas de execução:_ Blueprint `main` sem `url_prefix`, com `GET /`, `/login`, `/register`
via `render_template`. `base.html` tem charset/viewport, blocos `title` e `content`, e linka
o CSS por `url_for('static', ...)`. `main.css` é puro (sem framework): tema escuro, layout
centralizado e responsivo (`max-width` + flex). `landing.html` mostra "Prumo" + tagline e os
3 botões (Criar conta → `main.register`, Entrar → `main.login`, Entrar com Google →
`/auth/google` literal). `login.html` (email+senha) e `register.html` (nome+email+senha) têm
`method="post" action=""` ainda inativos, com links cruzados. `app.py` registra `main_bp`
após `health_bp`. **Verificado localmente** (venv em `app/.venv`, porta 5050): `/`=200 com os
3 botões, `/login`=200, `/register`=200 com seus campos, `/auth/google`=404 (esperado),
`/health/hello`=200 JSON intacto, CSS=200.

---

### [x] Etapa 6 — Autenticação email/senha

**Objetivo:** Register, login e logout funcionando; rotas protegidas redirecionam para `/login`.

Arquivos:
@app/routes/auth/__init__.py
@app/routes/auth/auth.py
@app/services/auth_service.py
@app/models/user.py
@app/app.py
@app/templates/auth/login.html
@app/templates/auth/register.html

- [x] `app/models/user.py` — adicionar `set_password(senha)` e `check_password(senha)` via `werkzeug.security`; implementar `UserMixin`
- [x] `app/services/auth_service.py` — `register_user(name, email, password)` e `authenticate_user(email, password)`
- [x] `app/routes/auth/auth.py` — Blueprint `auth` com prefixo `/auth`:
  - `POST /auth/register` — valida, chama `register_user`, faz login, redirect `/dashboard`
  - `POST /auth/login` — chama `authenticate_user`, faz login, redirect `/dashboard`
  - `GET /auth/logout` — `logout_user()`, redirect `/`
- [x] `app/app.py` — configurar `LoginManager`; `login_view = "main.login"`; registrar blueprint `auth`
- [x] `app/routes/main/main.py` — rota `GET /dashboard` com `@login_required`; retorna template placeholder `"Olá {{ current_user.name }}"` (será substituído na Etapa 8)
- [x] Ativar `action` e `method` nos templates `login.html` e `register.html`

**Verificar:**
1. `/register` → preencher → submit → redirect `/dashboard` mostrando "Olá <nome>"
2. `/auth/logout` → redirect `/`
3. `/dashboard` sem login → redirect `/login`
4. `/login` com credenciais → dashboard

_Notas de execução:_ `User` agora herda `UserMixin` e tem `set_password`/`check_password`
(werkzeug `generate_password_hash`/`check_password_hash`; `check_password` retorna `False`
se `password_hash` for nulo — usuários Google futuros). `services/auth_service.py` criado com
`register_user` (normaliza email para lowercase, rejeita email duplicado), `authenticate_user`
e uma exceção `AuthError` com mensagem amigável. `routes/auth/auth.py` é o blueprint `auth`
(`url_prefix="/auth"`): `POST /auth/register`, `POST /auth/login` (em erro, re-renderizam o
template com `flash` e status 400/401), `GET /auth/logout` (`@login_required`). `app.py`
instancia `LoginManager` no módulo, `login_view = "main.login"` (a rota que serve o form de
login é `main.login`, não existe `auth.login_page`), com `user_loader` via
`db.session.get(User, int(id))`. `main.py` ganhou `GET /dashboard` com `@login_required`
renderizando `dashboard_placeholder.html` (saudação + link Sair — será trocado na Etapa 8).
`base.html` passou a exibir `get_flashed_messages()` num bloco `.flash` (CSS vermelho em
`main.css`). Forms `login.html`/`register.html` com `action` apontando para `auth.login`/
`auth.register`.

**Verificado localmente** (venv `app/.venv`, porta 5050, após `create-db`):
(1) `POST /auth/register` → 302 → `/dashboard`; dashboard com cookie de sessão = 200 "Olá Maria
Teste". (2) `GET /auth/logout` com sessão → 302 → `/`. (3) `/dashboard` sem login → 302 →
`/login?next=%2Fdashboard`; idem após logout. (4) `POST /auth/login` com credenciais corretas →
302 → `/dashboard`. Erros: senha errada → 401 com flash "E-mail ou senha inválidos."; email
duplicado no register → 400 com flash "Já existe uma conta com esse e-mail." `/health/hello`
intacto (200). Usuário de teste removido do `prumo.db` ao final.

---

### [x] Etapa 7 — Google OAuth

**Objetivo:** "Entrar com Google" autentica via Authlib; usuário Google criado/associado no banco; chega no dashboard.

Arquivos:
@app/routes/auth/auth.py
@app/services/auth_service.py
@app/app.py
@app/.env.example

**Passos no Google Cloud Console (usuário executa):**

- [x] console.cloud.google.com → projeto "Prumo" criado
- [x] APIs & Services → OAuth consent screen → External configurado
- [x] Credentials → OAuth 2.0 Client ID → Web application:
  - Redirect URI prod: `https://app.prumoboard.com/auth/google/callback`
  - Redirect URI dev: `http://localhost:5000/auth/google/callback`
  - `GOOGLE_CLIENT_ID` e `GOOGLE_CLIENT_SECRET` no `.env` local e do servidor

**Código (Claude executa):**

- [x] `app/oauth.py` — `OAuth` do Authlib inicializado; cliente Google com `server_metadata_url`
- [x] `app/routes/auth/auth.py` — `GET /auth/google/login` e `GET /auth/google/callback`
- [x] `app/services/auth_service.py` — `find_or_create_google_user(userinfo)`

**Verificar:**
1. `.env` com `GOOGLE_CLIENT_ID` e `GOOGLE_CLIENT_SECRET` reais ✓
2. `flask --app app run --debug`
3. Landing page → "Entrar com Google" → consent Google → dashboard com "Olá <nome Google>"
4. `flask shell` → `User.query.filter(User.google_id != None).all()` → retorna o usuário

_Notas de execução:_ Código implementado no commit `c26bc44`. Credenciais Google configuradas pelo
usuário em 21/06/2026. Verificado: `/auth/google/login` retorna 302 → `accounts.google.com` em dev
e em produção. Fluxo completo de consentimento (browser) — testar manualmente.

---

### [x] Etapa 8 — Dashboard mínimo com "Sair"

**Objetivo:** Dashboard real com saudação e botão "Sair" funcional; verificação end-to-end em produção.

Arquivos:
@app/templates/dashboard.html
@app/routes/main/main.py

- [x] `app/templates/dashboard.html` — estende `base.html`: saudação `"Olá, {{ current_user.name }}"` + botão/link "Sair" → `/auth/logout`
- [x] `app/routes/main/main.py` — rota `/dashboard` usa `render_template("dashboard.html")` (remover placeholder da Etapa 6)

**Verificar:**
1. Login (email/senha ou Google) → dashboard: ver nome + botão "Sair"
2. Clicar "Sair" → redirect `/`
3. `/dashboard` sem sessão → redirect `/login`
4. `git push` → `curl -i https://app.prumoboard.com/` → landing page HTML (não mais JSON)
5. `curl https://app.prumoboard.com/health/hello` → ainda retorna `{"message": "Hello Prumo"}`

_Notas de execução:_ `dashboard.html` criado estendendo `base.html`: card com "Prumo" (brand-sm),
saudação `current_user.name or current_user.email`, e botão "Sair" (`.btn .btn-secondary`) apontando
para `auth.logout`. `main.py` trocou `dashboard_placeholder.html` pelo `dashboard.html`. Verificado
localmente (porta 5050): `POST /auth/register` → 302; `/dashboard` com sessão → 200 com saudação
correta e link "Sair"; `/auth/logout` → 302 para `/`; `/dashboard` sem sessão → redirect para `/login`.

---

## Riscos / observações

- **Etapa 3 é inteiramente manual** (cPanel + GitHub). Não avançar para Etapa 4 sem `curl /health/hello` retornar 200 em produção.
- **Etapa 7 requer credenciais Google reais.** Usuário precisa criar o projeto no Cloud Console antes do código ser testado.
- **`prumo.db` nunca vai para o git** (`.gitignore`). Em prod o cPanel usa MySQL via `DATABASE_URL` no `.env` do servidor (configurado manualmente na hospedagem).
- **`.env` nunca vai para o git.** Só `.env.example` é commitado.
- **pip no cPanel:** na Etapa 3, o primeiro `pip install` precisa ser manual (virtualenv ativado no terminal cPanel). Das Etapas 4 em diante, o `.cpanel.yml` cuida automaticamente.
- **`site/.htaccess`** vai para `public_html/` do cPanel manualmente uma única vez (não precisa de Python App).
- **Rotas de diagnóstico permanecem para sempre.** O endpoint `/health/hello` deve responder em todas as etapas — é o canário do deploy.
- **Deploy via webhook (`POST /health/deploy`)** substituiu a API instável do cPanel durante a Etapa 4. Detalhes completos, dores, dificuldades e comandos manuais na seção **"Desvio da Etapa 4 — Deploy via webhook"** abaixo.

---

## Desvio da Etapa 4 — Deploy via webhook

> Registro da sessão de 21/06/2026. Não fazia parte da Etapa 4 original; surgiu de
> uma dor real de deploy durante a execução dela. Documenta o que foi feito, as
> dores que existiam antes, as dificuldades enfrentadas e como cada uma foi resolvida.

### O que foi entregue

**Etapa 4 do handoff — Banco de dados e modelo User**
- **`app/database.py`** — `db = SQLAlchemy()` e `init_db(app)` que chama `db.init_app(app)`.
- **`app/models/user.py`** — modelo `User` com `id`, `email` (unique, not null),
  `name`, `password_hash` (nullable), `google_id` (nullable), `created_at`.
- **`app/app.py`** — chama `init_db(app)`, importa `User` dentro da factory e expõe
  o comando CLI `flask create-db` (roda `db.create_all()`).
- **`app/config.py`** — `DATABASE_URL` do `.env` mapeado para `SQLALCHEMY_DATABASE_URI`.
- **`.gitignore`** — banco local `app/instance/prumo.db` fora do git.
- Verificado: `flask create-db` cria o banco; `User.query.all()` retorna `[]`.

**Novo mecanismo de deploy (não estava no handoff — surgiu de uma dor real)**
- **`POST /health/deploy`** — endpoint no blueprint `health`, protegido por Bearer
  token, que roda `git pull origin main` + `pip install` + `touch tmp/restart.txt`
  direto no servidor.
- **`passenger_wsgi.py`** — passou a carregar o `.env` via `load_dotenv()`.
- **`deploy.py`** — reescrito: agora chama o endpoint HTTP em vez da API do cPanel.
- **`config.py`** / **`.env.example`** / **`deploy.ini`** — `DEPLOY_TOKEN` adicionado.

### A dor que existia antes

**O deploy não era confiável.** O fluxo anterior do `deploy.py` chamava a API do
cPanel (`VersionControl/update` + `VersionControlDeployment/create`). O problema:
a chamada `VersionControl/update` **retornava `status: 1` (sucesso) sem de fato
executar o `git pull`**. Resultado: o script dizia "Deploy concluído" mas o código
novo nunca chegava ao servidor — era preciso entrar no cPanel e clicar manualmente
em "Update from Remote" toda vez.

O GitHub Actions (`deploy.yml`) resolveria isso via SSH, mas está **bloqueado por
pendência de billing**, então não era opção.

### Dificuldades enfrentadas e como foram resolvidas

**Dificuldade 1 — `flask create-db` criava o banco, mas sem a tabela `users`**
Sintoma: `flask create-db` rodava sem erro, mas `User.query.all()` falhava com
`OperationalError: no such table: users`.
Causa: `db.create_all()` só cria as tabelas dos modelos **registrados no metadata**
do SQLAlchemy. Como `User` nunca era importado, o metadata estava vazio.
Solução: importar `from models.user import User` **dentro de `create_app()`**
(com `# noqa: F401`), garantindo o registro antes do `create_all()`. Deletei o
`prumo.db` vazio e recriei.

**Dificuldade 2 — onde o Flask cria o SQLite**
Sintoma: `Test-Path app\prumo.db` retornava `False` mesmo após criar o banco.
Causa: o Flask grava o SQLite na pasta `instance/`, não na raiz de `app/`.
Solução: corrigi o `.gitignore` para `app/instance/prumo.db`.

**Dificuldade 3 — descobrir a porta SSH do cPanel (tentativa de fix permanente)**
Para tornar o `deploy.py` confiável, a primeira ideia foi usar SSH direto (como o
GitHub Actions faz). Gerei um par de chaves ed25519, importei e autorizei a chave
pública no cPanel. Mas a conexão falhava em todas as portas testadas:
- Porta 22 → `Connection refused` (bloqueada por firewall da hospedagem).
- Porta 26 → respondia, mas era **SMTP (Exim)**, não SSH.
- Portas 2222, 65002, 22022, 2200, etc. → `timeout`.
- Um scan de portas só achou serviços web/FTP/DNS — nenhum SSH exposto.
- O terminal do cPanel é um ambiente **restrito (jail)**: sem `ss`, sem `netstat`
  útil, sem acesso a `/etc/ssh/sshd_config`.

Conclusão: SSH externo está bloqueado nesta hospedagem compartilhada. Abandonei
essa rota.

**Dificuldade 4 — alternativa ao SSH: endpoint de deploy no próprio app**
Como o app já responde via HTTPS, criei um endpoint `POST /health/deploy` que faz o
trabalho do SSH (`git pull` + `pip install` + restart) de dentro do servidor,
protegido por um Bearer token secreto. O `deploy.py` virou um simples cliente HTTP.

**Dificuldade 5 — o endpoint subia, mas o token não era lido (`503 deploy not configured`)**
Sintoma: o endpoint respondia, mas `current_app.config["DEPLOY_TOKEN"]` vinha vazio.
Causa: em produção o Passenger usa `passenger_wsgi.py` diretamente — **não passa por
`flask run`**, que é quem normalmente carrega o `.env`. Logo o `.env` nunca era lido.
Solução: adicionar `load_dotenv()` no topo do `passenger_wsgi.py`, apontando para
`app/.env`.

**Dificuldade 6 — Passenger não reiniciava sozinho após o "Update from Remote"**
Sintoma: o código novo chegava ao servidor (confirmado via `tail` no arquivo), mas o
app continuava servindo a versão antiga; o `/health/deploy` dava 404.
Causa: "Update from Remote" do cPanel só faz o `git pull` — **não roda as tarefas do
`.cpanel.yml`** (que incluem o `touch tmp/restart.txt`).
Solução: durante o bootstrap, forçar o restart manualmente (`touch tmp/restart.txt`
no terminal). Depois que o endpoint `/health/deploy` passou a existir e funcionar,
ele mesmo cuida do restart — o problema desaparece.

### Estado final do fluxo de deploy

A partir de agora, o ciclo é:
```bash
git push origin main
python deploy.py        # POST /health/deploy → git pull + pip install + restart
```
O `deploy.py` agora mostra o diff real do `git pull` (ex.: `Fast-forward ...`),
confirmando que o código chegou — fim da incerteza do fluxo antigo.

**Observação sobre o bootstrap:** as dificuldades 5 e 6 foram **dores de
ovo-e-galinha** — o mecanismo que conserta o deploy precisava, ele mesmo, ser
deployado por um meio confiável. Por isso os primeiros commits desta sessão ainda
dependeram de `git pull` + `touch tmp/restart.txt` manuais no terminal do cPanel. Do
commit que corrigiu o `load_dotenv` em diante, `python deploy.py` passou a funcionar
sozinho.

### Commits da sessão
- `ff944b1` — feat: etapa 4 — SQLAlchemy + modelo User + comando create-db
- `9530bb0` — feat: endpoint POST /health/deploy para deploy via webhook
- `d67d73f` — fix: load_dotenv no passenger_wsgi para carregar .env em produção
- `e681b85` — docs: CLAUDE.md — deploy via webhook, etapa 4 concluída

### Comandos úteis (execução manual / troubleshooting)

Caminhos no servidor:
- App root: `/home/bianchin/public_html/app.prumoboard.com`
- venv pip: `/home/bianchin/virtualenv/public_html/app.prumoboard.com/3.13/bin/pip`
- `.env`: `/home/bianchin/public_html/app.prumoboard.com/app/.env`

**Deploy normal (o caminho feliz)**
```bash
git push origin main
python deploy.py        # POST /health/deploy → git pull + pip install + restart
```

**Disparar o deploy direto via HTTP (sem o deploy.py)** — token em `deploy.ini [deploy] token`:
```powershell
$h = @{ Authorization = "Bearer <DEPLOY_TOKEN>" }
Invoke-WebRequest -Uri "https://app.prumoboard.com/health/deploy" -Method POST -Headers $h -TimeoutSec 60 | Select-Object StatusCode, Content
```
```bash
curl -i -X POST https://app.prumoboard.com/health/deploy -H "Authorization: Bearer <DEPLOY_TOKEN>"
```

**Deploy manual pelo terminal do cPanel (se o endpoint estiver fora do ar)** — faz na mão o que o endpoint faz:
```bash
cd /home/bianchin/public_html/app.prumoboard.com
git pull origin main
source /home/bianchin/virtualenv/public_html/app.prumoboard.com/3.13/bin/activate
pip install --prefer-binary -r app/requirements.txt
touch tmp/restart.txt        # reinicia o Passenger
```

**Forçar só o restart do Passenger**
```bash
touch /home/bianchin/public_html/app.prumoboard.com/tmp/restart.txt
```

**Confirmar que o app está no ar e em qual versão**
```bash
curl https://app.prumoboard.com/health/hello      # {"message":"Hello Prumo"}
curl https://app.prumoboard.com/health/            # status + timestamp
```
Conferir, pelo terminal do cPanel, se o código novo realmente chegou:
```bash
tail -5 /home/bianchin/public_html/app.prumoboard.com/app/routes/health/health.py
git -C /home/bianchin/public_html/app.prumoboard.com log --oneline -3
```

**Diagnosticar "deploy not configured" (HTTP 503)** — `DEPLOY_TOKEN` não foi lido:
```bash
cat /home/bianchin/public_html/app.prumoboard.com/app/.env
# se faltar a linha:
echo "DEPLOY_TOKEN=<TOKEN>" >> /home/bianchin/public_html/app.prumoboard.com/app/.env
touch /home/bianchin/public_html/app.prumoboard.com/tmp/restart.txt
```

**Banco de dados (local)**
```bash
cd app
flask --app app create-db          # cria app/instance/prumo.db + tabela users
flask --app app shell
>>> from models.user import User
>>> User.query.all()
```

**Gerar um novo token de deploy**
```bash
python -c "import secrets; print(secrets.token_hex(32))"
```
Depois atualizar nos dois lados: `deploy.ini [deploy] token` (local) e `DEPLOY_TOKEN`
no `.env` do servidor — e reiniciar o Passenger.

**Disparar as tarefas do .cpanel.yml via API (alternativa ao restart manual)**
```powershell
$h = @{ Authorization = "cpanel bianchin:<CPANEL_API_TOKEN>" }
$root = [uri]::EscapeDataString("/home/bianchin/public_html/app.prumoboard.com")
Invoke-WebRequest -Uri "https://bianch.in:2083/execute/VersionControlDeployment/create?repository_root=$root" -Headers $h -UseBasicParsing
```
> Nota: `VersionControl/update` (o git pull via API) é o que se mostrou **não
> confiável** — retorna sucesso sem puxar o código. Prefira o `/health/deploy`.
