@danhachuel/prodmaker 0.1.15 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,45 @@
3
3
  Todas as versões seguem a etiqueta visual `MAJOR.MINOR.PATCH` com patch de dois dígitos
4
4
  (o registry NPM normaliza `0.1.00` → `0.1.0`).
5
5
 
6
+ ## 0.1.17 — 2026-11-01
7
+
8
+ - **HOTFIX — servidor encerrando sozinho no Windows (CMD/MobaXterm/PowerShell).**
9
+ - Launcher: no Windows o processo Python é iniciado com `shell: true` além de
10
+ `stdio: 'inherit'`, mantendo o terminal anexado; nova mensagem explícita
11
+ "O servidor está rodando. Mantenha esta janela aberta. Pressione CTRL+C para encerrar.";
12
+ se o filho morrer em menos de 3 segundos com código ≠ 0, o launcher imprime o
13
+ traceback hint e o comando manual em vez de sair em silêncio; `child.on("error")`
14
+ também diagnosticado.
15
+ - **database.py à prova de crash:** `ensure_db()` recriado com try/except por
16
+ instrução (CREATE TABLE IF NOT EXISTS + ALTERs de migração) e recuperação de
17
+ arquivo SQLite corrompido — o banco ruim é movido para
18
+ `prodmaker.db.corrupt-AAAAMMDD-HHMMSS` e um novo é criado; o Uvicorn nunca cai
19
+ na inicialização por causa do banco.
20
+ - **Fallback manual:** `app/main.py` agora tem entry point (`python -m app.main`
21
+ sobe o uvicorn na porta 8188) para ver o traceback completo no terminal.
22
+ - README/MANual: seção "Se o servidor fechar sozinho" com o comando manual
23
+ `.\venv\Scripts\python.exe -m app.main` (na pasta do ProdMaker).
24
+
25
+ ## 0.1.16 — 2026-10-31
26
+
27
+ - **Fase 12 — Billing Stripe + E-mail Marketing (AdsAcademy) + Supabase multi-tenant pronto.**
28
+ - **Billing:** `seed/planos.json` (Local grátis / Lançador PRO R$97 / Escala AGENCY R$297),
29
+ página **Plano & Assinatura** com cards, e-mail de conta e botão Assinar (abre o
30
+ Checkout do Stripe em nova aba); `POST /api/billing/checkout`, `/api/billing/assinatura`,
31
+ `/api/billing/historico` e **webhook público** `/api/billing/webhook` com verificação
32
+ de assinatura HMAC-SHA256 (t=...,v1=...) processando checkout.session.completed,
33
+ invoice.paid e customer.subscription.deleted. Sem chaves Stripe o app segue 100%
34
+ liberado em modo local (nenhuma trava de pré-registo).
35
+ - **E-mail Marketing (AdsAcademy):** sequências com gatilho (`venda_recebida` dispara
36
+ automaticamente a cada webhook de venda, `produto_salvo`, `manual`), variáveis
37
+ {{nome}}/{{produto}}/{{valor}}/{{link}}, envio pelo apprise já integrado (mailtos://,
38
+ tgram://...), editor de sequências, envio manual e histórico com status/erro. A
39
+ plataforma completa emareach fica como backlog da nuvem.
40
+ - **Supabase multi-tenant:** `DATABASE_URL` agora também aceita o alias **`SUPABASE_DB_URL`**
41
+ (connection string do Supabase) mantendo SQLite como fallback — user_id já cobre as
42
+ 15 tabelas; a ativação do multi-tenant depende apenas do Supabase Auth (backlog).
43
+ - Novas tabelas: `assinaturas`, `email_sequencias`, `email_envios`.
44
+
6
45
  ## 0.1.15 — 2026-10-31
7
46
 
8
47
  - **Fase 11 — TikTok Ads real + Anti-Fraude AdsPixel (Redis).**
@@ -1,10 +1,10 @@
1
- # Manual de Instalação — ProdMaker
1
+ # Manual de Instala????o ??? ProdMaker
2
2
 
3
- ## Pré-requisitos
3
+ ## Pr??-requisitos
4
4
 
5
- - **Node.js 20 ou superior** — https://nodejs.org
6
- - **Python 3.10 ou superior** — https://python.org (marcar "Add to PATH" no Windows)
7
- - Internet para download do pacote npm e das dependências
5
+ - **Node.js 20 ou superior** ??? https://nodejs.org
6
+ - **Python 3.10 ou superior** ??? https://python.org (marcar "Add to PATH" no Windows)
7
+ - Internet para download do pacote npm e das depend??ncias
8
8
 
9
9
  ## Instalar
10
10
 
@@ -20,7 +20,7 @@ npx --yes @danhachuel/prodmaker@0.1.1 install
20
20
 
21
21
  O que o `install` faz:
22
22
 
23
- 1. Cria a pasta de dados **PRODMAKER_HOME** (ordem de resolução: variável `PRODMAKER_HOME` → `C:\Users\danha\Documents\REPOSITORIO\ProdMaker` → `%USERPROFILE%\ProdMaker`).
23
+ 1. Cria a pasta de dados **PRODMAKER_HOME** (ordem de resolu????o: vari??vel `PRODMAKER_HOME` ??? `C:\Users\danha\Documents\REPOSITORIO\ProdMaker` ??? `%USERPROFILE%\ProdMaker`).
24
24
  2. Cria o storage local: `storage/{state,pages,produtos,artifacts,downloads,logs}` + `settings.json`.
25
25
  3. Cria um ambiente virtual Python (`venv`) dentro do PRODMAKER_HOME e instala o `requirements.txt`.
26
26
 
@@ -34,44 +34,46 @@ npx.cmd --yes --prefer-online @danhachuel/prodmaker@0.1.1
34
34
  npx --yes @danhachuel/prodmaker@0.1.1
35
35
  ```
36
36
 
37
- O terminal imprimirá:
37
+ O terminal imprimir??:
38
38
 
39
39
  ```
40
- ┌─────────────────────────────────────────────┐
41
- │ ProdMaker rodando em: │
42
- │ >>> http://localhost:8188 <<< │
43
- │ Abra esta URL MANUALMENTE no navegador. │
44
- └─────────────────────────────────────────────┘
40
+ ?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
41
+ ??? ProdMaker rodando em: ???
42
+ ??? >>> http://localhost:8188 <<< ???
43
+ ??? Abra esta URL MANUALMENTE no navegador. ???
44
+ ?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
45
45
  ```
46
46
 
47
47
  **Abra a URL manualmente.** O ProdMaker nunca abre o navegador sozinho (REGRA ZERO).
48
- O box de inicialização também imprime o **Código de Ativação Mestre** (default: `1234`) —
49
- no login, use qualquer e-mail + esse código (ou digite `admin` como código).
50
- Código customizado: variável de ambiente `PRODMAKER_MASTER_CODE`.
51
- Se a porta 8188 estiver ocupada, o launcher usará automaticamente 8189–8199 e informará.
48
+ O box de inicializa????o tamb??m imprime o **C??digo de Ativa????o Mestre** (default: `1234`) ???
49
+ no login, use qualquer e-mail + esse c??digo (ou digite `admin` como c??digo).
50
+ C??digo customizado: vari??vel de ambiente `PRODMAKER_MASTER_CODE`.
51
+ Se a porta 8188 estiver ocupada, o launcher usar?? automaticamente 8189???8199 e informar??.
52
52
 
53
- - Login: informe qualquer e-mail e o **Código de Ativação Mestre** impresso no terminal ao iniciar (default: `1234`; ou digite `admin`).
53
+ - Login: informe qualquer e-mail e o **C??digo de Ativa????o Mestre** impresso no terminal ao iniciar (default: `1234`; ou digite `admin`).
54
54
  - Para encerrar: `Ctrl+C` no terminal.
55
55
 
56
- ## Diagnóstico
56
+ ## Diagn??stico
57
57
 
58
58
  ```bash
59
59
  npx.cmd --yes @danhachuel/prodmaker@0.1.0 doctor
60
60
  ```
61
61
 
62
- Verifica Node, Python, venv, dependências, storage, backend e porta.
62
+ Verifica Node, Python, venv, depend??ncias, storage, backend e porta.
63
63
 
64
- ## Solução de problemas
64
+ ## Solu????o de problemas
65
65
 
66
- | Problema | Solução |
66
+ | Problema | Solu????o |
67
67
  |---|---|
68
- | "Python não encontrado" | Instale o Python 3.10+ e rode `prodmaker install` novamente. |
69
- | Porta 8188 ocupada | O launcher migra para 8189–8199 automaticamente; confira a URL impressa. |
70
- | `settings.json corrompido` | O sistema preserva a cópia em `storage/state/settings.corrupt.json` e não sobrescreve. Restaure manualmente. |
68
+ | "Python n??o encontrado" | Instale o Python 3.10+ e rode `prodmaker install` novamente. |
69
+ | Porta 8188 ocupada | O launcher migra para 8189???8199 automaticamente; confira a URL impressa. |
70
+ | `settings.json corrompido` | O sistema preserva a c??pia em `storage/state/settings.corrupt.json` e n??o sobrescreve. Restaure manualmente. |
71
71
  | Erro no Windows sobre scripts desabilitados | Use `npx.cmd` (CMD/MobaXterm) em vez do `npx` do PowerShell. |
72
- | `'prodmaker' não é reconhecido` ao rodar o npx | Execute os comandos npx **fora da pasta do repositório** (ex.: `%USERPROFILE%`). Dentro da pasta do projeto, o npx prioriza o pacote local (que não tem `node_modules/.bin`). Para desenvolvimento local use `node scripts\cli.mjs` na raiz do repositório. |
72
+ | `'prodmaker' n??o ?? reconhecido` ao rodar o npx | Execute os comandos npx **fora da pasta do reposit??rio** (ex.: `%USERPROFILE%`). Dentro da pasta do projeto, o npx prioriza o pacote local (que n??o tem `node_modules/.bin`). Para desenvolvimento local use `node scripts\cli.mjs` na raiz do reposit??rio. |
73
+ | **O servidor fecha sozinho / terminal volta ao prompt** | Rode manualmente para ver o traceback completo: `.\venv\Scripts\python.exe -m app.main` (dentro da pasta do ProdMaker). O launcher diagnostica encerramentos em menos de 3 segundos com essa mesma instru????o. Banco corrompido ?? recuperado automaticamente (backup `prodmaker.db.corrupt-*`). |
73
74
 
74
75
  ## Importante
75
76
 
76
- - **Nunca rode os comandos npx dentro de `C:\Users\danha\Documents\REPOSITORIO\ProdMaker`** — o npx entende que você quer executar o projeto local. Use qualquer outra pasta.
77
- - O ProdMaker **nunca abre o navegador** (REGRA ZERO): após iniciar, abra manualmente a URL impressa no terminal.
77
+ - **Nunca rode os comandos npx dentro de `C:\Users\danha\Documents\REPOSITORIO\ProdMaker`** ??? o npx entende que voc?? quer executar o projeto local. Use qualquer outra pasta.
78
+ - O ProdMaker **nunca abre o navegador** (REGRA ZERO): ap??s iniciar, abra manualmente a URL impressa no terminal.
79
+
package/README.md CHANGED
@@ -1,43 +1,59 @@
1
1
  # ProdMaker
2
2
 
3
- 🚀 **SaaS local para criação de infoprodutos low-ticket, landing pages, campanhas de tráfego e tracking** — baseado no sistema AdsX, executado no seu computador via `npx` e acessado manualmente no navegador.
3
+ ???? **SaaS local para cria????o de infoprodutos low-ticket, landing pages, campanhas de tr??fego e tracking** ??? baseado no sistema AdsX, executado no seu computador via `npx` e acessado manualmente no navegador.
4
4
 
5
- > ⚠️ **REGRA ZERO:** o ProdMaker **nunca abre o navegador**. Ao iniciar, ele apenas imprime a URL (`http://localhost:8188`) no terminal — você abre manualmente.
5
+ > ?????? **REGRA ZERO:** o ProdMaker **nunca abre o navegador**. Ao iniciar, ele apenas imprime a URL (`http://localhost:8188`) no terminal ??? voc?? abre manualmente.
6
6
 
7
- ## Instalação
7
+ ## Instala????o
8
8
 
9
9
  ```bash
10
- # CMD/MobaXterm (Windows)
11
- npx.cmd --yes --prefer-online @danhachuel/prodmaker@0.1.1 install
12
- npx.cmd --yes --prefer-online @danhachuel/prodmaker@0.1.1
10
+ # CMD/MobaXterm (Windows) ??? fora da pasta do reposit??rio
11
+ npx.cmd --yes --prefer-online @danhachuel/prodmaker@0.1.17 install
12
+ npx.cmd --yes --prefer-online @danhachuel/prodmaker@0.1.17
13
13
 
14
14
  # Linux/macOS/PowerShell
15
- npx --yes @danhachuel/prodmaker@0.1.1 install
16
- npx --yes @danhachuel/prodmaker@0.1.1
15
+ npx --yes @danhachuel/prodmaker@0.1.17 install
16
+ npx --yes @danhachuel/prodmaker@0.1.17
17
17
  ```
18
18
 
19
- 1. `install` cria a pasta de dados (`PRODMAKER_HOME`), o storage local e instala as dependências Python.
19
+ 1. `install` cria a pasta de dados (`PRODMAKER_HOME`), o storage local e instala as depend??ncias Python.
20
20
  2. O segundo comando inicia o servidor em `http://localhost:8188` e imprime a URL no terminal.
21
- 3. **Abra a URL manualmente no navegador**, faça login (qualquer e-mail + o **Código de Ativação Mestre** impresso no terminal — default `1234` — ou `admin`) e acesse o painel.
21
+ 3. **Abra a URL manualmente no navegador**, fa??a login (qualquer e-mail + o **C??digo de Ativa????o Mestre** impresso no terminal ??? default `1234` ??? ou `admin`) e acesse o painel.
22
22
 
23
- ## O que está incluído (0.1.01 — Parte 2: Frontend)
23
+ ## Se o servidor fechar sozinho no Windows
24
24
 
25
- | Área | Estado |
25
+ Se o terminal voltar para o prompt sem manter o servidor (ou travar), rode manualmente
26
+ para ver o traceback completo:
27
+
28
+ ```bash
29
+ # dentro da pasta do ProdMaker (PRODMAKER_HOME)
30
+ .\venv\Scripts\python.exe -m app.main
31
+ ```
32
+
33
+ O comando acima sobe o servidor na porta 8188 com o log completo no terminal. O launcher
34
+ tamb??m passou a diagnosticar encerramento imediato (menos de 3 segundos) imprimindo essa
35
+ mesma instru????o. Se o banco local estiver corrompido, o sistema faz backup autom??tico
36
+ (`prodmaker.db.corrupt-*`) e recria ??? o servidor n??o cai mais na inicializa????o.
37
+
38
+ ## O que est?? inclu??do (0.1.01 ??? Parte 2: Frontend)
39
+
40
+ | ??rea | Estado |
26
41
  |---|---|
27
- | Launcher npm (`install` / `run` / `doctor`) | ✅ |
28
- | Backend FastAPI (auth local, settings, health) | ✅ |
29
- | Storage persistente local (fora do cache npm) | ✅ |
30
- | Seeds de negócio (prompts, negativas, defaults, decisão, checklist) | ✅ |
31
- | `/legal` (index, privacidade LGPD, termos) | ✅ |
32
- | Workflow de publicação NPM via GitHub Actions | ✅ |
33
- | Frontend React + Vite + Tailwind (login + painel AdsX) | ✅ |
34
- | Seus Produtos (CRUD real: análise → aprovação) | ✅ |
35
- | Tema claro/escuro | ✅ |
42
+ | Launcher npm (`install` / `run` / `doctor`) | ??? |
43
+ | Backend FastAPI (auth local, settings, health) | ??? |
44
+ | Storage persistente local (fora do cache npm) | ??? |
45
+ | Seeds de neg??cio (prompts, negativas, defaults, decis??o, checklist) | ??? |
46
+ | `/legal` (index, privacidade LGPD, termos) | ??? |
47
+ | Workflow de publica????o NPM via GitHub Actions | ??? |
48
+ | Frontend React + Vite + Tailwind (login + painel AdsX) | ??? |
49
+ | Seus Produtos (CRUD real: an??lise ??? aprova????o) | ??? |
50
+ | Tema claro/escuro | ??? |
36
51
 
37
52
  ## Estrutura
38
53
 
39
- Consulte o [Planejamento Único](./PRODMAKER%20—%20PLANEJAMENTO%20ÚNICO%20E%20COMPLETO.md) para a visão completa (arquitetura, UI AdsX, módulos, fases e regras), o [Manual de Instalação](./MANUAL-INSTALACAO.md) e as [Regras do Repositório](./AGENTS.md).
54
+ Consulte o [Planejamento ??nico](./PRODMAKER%20???%20PLANEJAMENTO%20??NICO%20E%20COMPLETO.md) para a vis??o completa (arquitetura, UI AdsX, m??dulos, fases e regras), o [Manual de Instala????o](./MANUAL-INSTALACAO.md) e as [Regras do Reposit??rio](./AGENTS.md).
55
+
56
+ ## Licen??a
40
57
 
41
- ## Licença
58
+ MIT ??? ver [LICENSE](./LICENSE) e [THIRD-PARTY-NOTICES](./THIRD-PARTY-NOTICES.md).
42
59
 
43
- MIT — ver [LICENSE](./LICENSE) e [THIRD-PARTY-NOTICES](./THIRD-PARTY-NOTICES.md).
@@ -0,0 +1,63 @@
1
+ """Billing — planos (seed), checkout Stripe, webhook público e status da assinatura."""
2
+ from fastapi import APIRouter, HTTPException, Request
3
+ from fastapi.responses import Response
4
+ from pydantic import BaseModel
5
+
6
+ from ..services import stripe_billing
7
+ from ..services.llm import LLMError
8
+
9
+ router = APIRouter(prefix="/api/billing", tags=["billing"])
10
+
11
+
12
+ class CheckoutBody(BaseModel):
13
+ plano_id: str
14
+ email: str
15
+
16
+
17
+ @router.get("/planos")
18
+ def planos():
19
+ return stripe_billing.planos_local()
20
+
21
+
22
+ @router.get("/assinatura")
23
+ def assinatura():
24
+ return stripe_billing.assinatura_atual()
25
+
26
+
27
+ @router.get("/historico")
28
+ def historico():
29
+ return stripe_billing.historico()
30
+
31
+
32
+ @router.post("/checkout")
33
+ def checkout(body: CheckoutBody, request: Request):
34
+ base = f"{request.url.scheme}://{request.headers.get('host', 'localhost:8188')}"
35
+ try:
36
+ return stripe_billing.criar_checkout(
37
+ body.plano_id,
38
+ body.email,
39
+ f"{base}/configuracoes?checkout=ok",
40
+ f"{base}/configuracoes?checkout=cancelado",
41
+ )
42
+ except LLMError as exc:
43
+ raise HTTPException(status_code=400, detail=str(exc))
44
+
45
+
46
+ @router.options("/webhook")
47
+ def webhook_options():
48
+ return Response(status_code=204)
49
+
50
+
51
+ @router.post("/webhook")
52
+ async def webhook_stripe(request: Request):
53
+ """Webhook público do Stripe — verifica assinatura e processa o evento."""
54
+ body = await request.body()
55
+ if not stripe_billing.verificar_assinatura(body, request.headers.get("stripe-signature", "")):
56
+ raise HTTPException(status_code=400, detail="Assinatura do webhook inválida.")
57
+ import json
58
+
59
+ try:
60
+ evento = json.loads(body)
61
+ except json.JSONDecodeError:
62
+ raise HTTPException(status_code=400, detail="Payload inválido.")
63
+ return stripe_billing.processar_webhook(evento)
@@ -0,0 +1,65 @@
1
+ """E-mail Marketing (AdsAcademy) — sequências com gatilho, envio manual e histórico."""
2
+ from fastapi import APIRouter, HTTPException
3
+ from pydantic import BaseModel
4
+
5
+ from ..services import email_mkt
6
+ from ..services.llm import LLMError
7
+
8
+ router = APIRouter(prefix="/api/email", tags=["email"])
9
+
10
+
11
+ class SequenciaBody(BaseModel):
12
+ nome: str
13
+ gatilho: str
14
+ assunto: str
15
+ corpo: str
16
+ ativo: bool = True
17
+
18
+
19
+ class EnviarBody(BaseModel):
20
+ sequencia_id: int
21
+ destinos: list = []
22
+ variaveis: dict = {}
23
+
24
+
25
+ @router.get("/sequencias")
26
+ def sequencias():
27
+ return email_mkt.listar()
28
+
29
+
30
+ @router.post("/sequencias")
31
+ def criar(body: SequenciaBody):
32
+ try:
33
+ return email_mkt.salvar(body.nome, body.gatilho, body.assunto, body.corpo, body.ativo)
34
+ except LLMError as exc:
35
+ raise HTTPException(status_code=400, detail=str(exc))
36
+
37
+
38
+ @router.put("/sequencias/{sequencia_id}")
39
+ def atualizar(sequencia_id: int, body: SequenciaBody):
40
+ try:
41
+ return email_mkt.salvar(body.nome, body.gatilho, body.assunto, body.corpo, body.ativo, sequencia_id)
42
+ except LLMError as exc:
43
+ raise HTTPException(status_code=400, detail=str(exc))
44
+
45
+
46
+ @router.delete("/sequencias/{sequencia_id}")
47
+ def remover(sequencia_id: int):
48
+ try:
49
+ email_mkt.remover(sequencia_id)
50
+ except LLMError as exc:
51
+ raise HTTPException(status_code=404, detail=str(exc))
52
+ return {"ok": True}
53
+
54
+
55
+ @router.post("/enviar")
56
+ def enviar(body: EnviarBody):
57
+ try:
58
+ return email_mkt.enviar_manual(body.sequencia_id, body.destinos, body.variaveis)
59
+ except LLMError as exc:
60
+ raise HTTPException(status_code=400, detail=str(exc))
61
+
62
+
63
+ @router.get("/historico")
64
+ def historico():
65
+ return email_mkt.historico()
package/app/main.py CHANGED
@@ -15,7 +15,7 @@ from .services.database import ensure_db, get_db, now_str, row_to_dict
15
15
  from .services.llm import LLMError, analyze_product, list_providers
16
16
  from .services.store import PRODMAKER_HOME, STORAGE, STATE, ensure_state_dir, get_master_code, get_settings, save_settings
17
17
 
18
- APP_VERSION = "0.1.15"
18
+ APP_VERSION = "0.1.17"
19
19
  COOKIE_NAME = "prodmaker_session"
20
20
  SESSION_DAYS = 30
21
21
  MASTER_ALIAS = "admin"
@@ -43,6 +43,8 @@ from .api import invest as invest_router # noqa: E402
43
43
  from .api import builder as builder_router # noqa: E402
44
44
  from .api import fraude as fraude_router # noqa: E402
45
45
  from .api import tiktok as tiktok_router # noqa: E402
46
+ from .api import billing as billing_router # noqa: E402
47
+ from .api import email_mkt as email_mkt_router # noqa: E402
46
48
 
47
49
  app.include_router(llm_router.router)
48
50
  app.include_router(infoprodutos_router.router)
@@ -62,6 +64,8 @@ app.include_router(invest_router.router)
62
64
  app.include_router(builder_router.router)
63
65
  app.include_router(fraude_router.router)
64
66
  app.include_router(tiktok_router.router)
67
+ app.include_router(billing_router.router)
68
+ app.include_router(email_mkt_router.router)
65
69
 
66
70
  ensure_db()
67
71
 
@@ -268,7 +272,7 @@ def health():
268
272
 
269
273
  @app.get("/api/version")
270
274
  def version():
271
- return {"version": APP_VERSION, "canonical": "0.1.15"}
275
+ return {"version": APP_VERSION, "canonical": "0.1.17"}
272
276
 
273
277
 
274
278
  @app.get("/api/settings")
@@ -388,3 +392,16 @@ def spa_fallback(full_path: str):
388
392
  if full_path and candidate.is_file():
389
393
  return FileResponse(candidate)
390
394
  return _spa_index()
395
+
396
+
397
+ # ---------------------------------------------------------------------------
398
+ # Entry point manual — permite rodar sem o launcher (útil para ver tracebacks):
399
+ # .\venv\Scripts\python.exe app/main.py
400
+ # ---------------------------------------------------------------------------
401
+
402
+ if __name__ == "__main__":
403
+ import uvicorn
404
+
405
+ porta = int(os.environ.get("PRODMAKER_PORT") or 8188)
406
+ print(f"[ProdMaker] Iniciando em http://127.0.0.1:{porta} — mantenha esta janela aberta (CTRL+C para encerrar).")
407
+ uvicorn.run(app, host="127.0.0.1", port=porta)
@@ -3,6 +3,9 @@
3
3
  Esquema unico para os dois engines. Todas as tabelas carregam `user_id` (preparacao
4
4
  multi-tenant — hoje vazio). `ensure_db()` cria o que falta e aplica ALTERs de migracao.
5
5
  """
6
+ import sqlite3
7
+ import time
8
+
6
9
  from .dbengine import (
7
10
  add_column_if_missing,
8
11
  is_postgres,
@@ -221,6 +224,42 @@ TABLES = {
221
224
  created_at TEXT NOT NULL
222
225
  )
223
226
  """,
227
+ "assinaturas": """
228
+ CREATE TABLE IF NOT EXISTS assinaturas (
229
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
230
+ user_id TEXT NOT NULL DEFAULT '',
231
+ plano TEXT NOT NULL DEFAULT '',
232
+ email TEXT NOT NULL DEFAULT '',
233
+ stripe_ref TEXT NOT NULL DEFAULT '',
234
+ status TEXT NOT NULL DEFAULT 'checkout',
235
+ current_period_end TEXT NOT NULL DEFAULT '',
236
+ created_at TEXT NOT NULL
237
+ )
238
+ """,
239
+ "email_sequencias": """
240
+ CREATE TABLE IF NOT EXISTS email_sequencias (
241
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
242
+ user_id TEXT NOT NULL DEFAULT '',
243
+ nome TEXT NOT NULL,
244
+ gatilho TEXT NOT NULL DEFAULT 'manual',
245
+ assunto TEXT NOT NULL,
246
+ corpo TEXT NOT NULL,
247
+ ativo INTEGER NOT NULL DEFAULT 1,
248
+ created_at TEXT NOT NULL
249
+ )
250
+ """,
251
+ "email_envios": """
252
+ CREATE TABLE IF NOT EXISTS email_envios (
253
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
254
+ user_id TEXT NOT NULL DEFAULT '',
255
+ sequencia_id INTEGER NOT NULL DEFAULT 0,
256
+ destino TEXT NOT NULL DEFAULT '',
257
+ assunto TEXT NOT NULL DEFAULT '',
258
+ status TEXT NOT NULL DEFAULT 'pendente',
259
+ erro TEXT NOT NULL DEFAULT '',
260
+ created_at TEXT NOT NULL
261
+ )
262
+ """,
224
263
  }
225
264
 
226
265
  ALL_TABLES = list(TABLES.keys())
@@ -237,19 +276,96 @@ def get_db():
237
276
  return connect()
238
277
 
239
278
 
240
- def ensure_db() -> None:
241
- with get_db() as conn:
242
- if is_postgres():
243
- for ddl in TABLES.values():
279
+ def _create_tables(conn) -> list[str]:
280
+ """Cria todas as tabelas; erros por instrução são coletados, nunca abortam."""
281
+ erros: list[str] = []
282
+ if is_postgres():
283
+ for nome, ddl in TABLES.items():
284
+ try:
244
285
  conn.execute(_pg_ddl(ddl))
245
- else:
246
- for ddl in TABLES.values():
247
- conn.execute(ddl)
248
- # Migracao: bancos criados antes da coluna user_id / da tabela investimentos
249
- for table in ALL_TABLES:
250
- add_column_if_missing(conn, table, "user_id TEXT NOT NULL DEFAULT ''", "user_id")
251
- conn.execute("CREATE INDEX IF NOT EXISTS idx_vendas_gclid ON vendas (gclid)")
252
- conn.execute("CREATE INDEX IF NOT EXISTS idx_visitas_gclid ON visitas (gclid)")
286
+ except Exception as exc: # noqa: BLE001
287
+ erros.append(f"{nome}: {exc}")
288
+ return erros
289
+ for nome, ddl in TABLES.items():
290
+ try:
291
+ conn.execute(ddl)
292
+ except sqlite3.DatabaseError as exc:
293
+ erros.append(f"{nome}: {exc}")
294
+ # Migracao: bancos criados antes da coluna user_id / das novas tabelas
295
+ for table in ALL_TABLES:
296
+ try:
297
+ add_column_if_missing(conn, table, "user_id TEXT NOT NULL DEFAULT ''", "user_id")
298
+ except Exception as exc: # noqa: BLE001
299
+ erros.append(f"user_id/{table}: {exc}")
300
+ for index in ("CREATE INDEX IF NOT EXISTS idx_vendas_gclid ON vendas (gclid)", "CREATE INDEX IF NOT EXISTS idx_visitas_gclid ON visitas (gclid)"):
301
+ try:
302
+ conn.execute(index)
303
+ except sqlite3.DatabaseError:
304
+ pass
305
+ return erros
306
+
307
+
308
+ def _sqlite_file():
309
+ from .dbengine import _sqlite_path
310
+
311
+ return _sqlite_path()
312
+
313
+
314
+ _CORRUPCAO_MARCADORES = ("not a database", "malformed", "encrypted", "corrupt", "database disk image")
315
+
316
+
317
+ def _parece_corrupcao(exc: Exception) -> bool:
318
+ texto = str(exc).lower()
319
+ return any(marcador in texto for marcador in _CORRUPCAO_MARCADORES)
320
+
321
+
322
+ def _recriar_banco() -> None:
323
+ arquivo = _sqlite_file()
324
+ backup = arquivo.with_name(f"prodmaker.db.corrupt-{time.strftime('%Y%m%d-%H%M%S')}")
325
+ try:
326
+ arquivo.rename(backup)
327
+ print(f"[ProdMaker] Banco SQLite corrompido movido para: {backup.name} — recriando vazio.")
328
+ except OSError:
329
+ try:
330
+ arquivo.unlink(missing_ok=True)
331
+ print("[ProdMaker] Banco SQLite corrompido removido — recriando vazio.")
332
+ except OSError:
333
+ print("[ProdMaker] Banco SQLite corrompido e não pode ser removido.")
334
+ return
335
+ with get_db() as conn:
336
+ _create_tables(conn)
337
+
338
+
339
+ def ensure_db() -> None:
340
+ """Cria/migra o banco. Se o arquivo SQLite estiver CORROMPIDO, faz backup e recria
341
+ (o Uvicorn nunca cai na inicialização por causa do banco)."""
342
+ if is_postgres():
343
+ with get_db() as conn:
344
+ _create_tables(conn)
345
+ return
346
+ conn = get_db()
347
+ try:
348
+ try:
349
+ _create_tables(conn)
350
+ # Probe de integridade: leitura real detecta arquivo corrompido.
351
+ conn.execute("SELECT COUNT(*) FROM produtos").fetchone()
352
+ return
353
+ except sqlite3.DatabaseError as exc:
354
+ if not _parece_corrupcao(exc):
355
+ # Erro de disco/permissão etc: não descarta o arquivo do usuário.
356
+ print(f"[ProdMaker] Aviso na inicialização do banco: {exc}")
357
+ return
358
+ corrompido = True
359
+ finally:
360
+ # IMPORTANTE (Windows): o context manager do sqlite3 NÃO fecha a conexão;
361
+ # sem close() o arquivo fica travado e o rename do recovery falha.
362
+ try:
363
+ conn.close()
364
+ except Exception: # noqa: BLE001
365
+ pass
366
+ if not corrompido:
367
+ return
368
+ _recriar_banco()
253
369
 
254
370
 
255
371
  ensure_db()
@@ -31,7 +31,8 @@ def row_to_dict(row) -> dict:
31
31
 
32
32
 
33
33
  def dsn() -> str | None:
34
- return (os.environ.get("DATABASE_URL") or "").strip() or None
34
+ """Connection string do PostgreSQL — aceita DATABASE_URL ou SUPABASE_DB_URL."""
35
+ return (os.environ.get("DATABASE_URL") or os.environ.get("SUPABASE_DB_URL") or "").strip() or None
35
36
 
36
37
 
37
38
  def is_postgres() -> bool: