@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 +39 -0
- package/MANUAL-INSTALACAO.md +29 -27
- package/README.md +40 -24
- package/app/api/billing.py +63 -0
- package/app/api/email_mkt.py +65 -0
- package/app/main.py +19 -2
- package/app/services/database.py +128 -12
- package/app/services/dbengine.py +2 -1
- package/app/services/email_mkt.py +146 -0
- package/app/services/stripe_billing.py +147 -0
- package/app/services/vendas.py +14 -0
- package/frontend/dist/assets/index-B6BGGw5j.js +80 -0
- package/frontend/dist/assets/index-Feb3evdj.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/scripts/cli.mjs +40 -18
- package/seed/planos.json +56 -0
- package/frontend/dist/assets/index-B4wxnTCs.css +0 -1
- package/frontend/dist/assets/index-rK4wl-bk.js +0 -78
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).**
|
package/MANUAL-INSTALACAO.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# Manual de
|
|
1
|
+
# Manual de Instala????o ??? ProdMaker
|
|
2
2
|
|
|
3
|
-
## Pr
|
|
3
|
+
## Pr??-requisitos
|
|
4
4
|
|
|
5
|
-
- **Node.js 20 ou superior**
|
|
6
|
-
- **Python 3.10 ou superior**
|
|
7
|
-
- Internet para download do pacote npm e das
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
49
|
-
no login, use qualquer e-mail + esse
|
|
50
|
-
|
|
51
|
-
Se a porta 8188 estiver ocupada, o launcher
|
|
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 **
|
|
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
|
-
##
|
|
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,
|
|
62
|
+
Verifica Node, Python, venv, depend??ncias, storage, backend e porta.
|
|
63
63
|
|
|
64
|
-
##
|
|
64
|
+
## Solu????o de problemas
|
|
65
65
|
|
|
66
|
-
| Problema |
|
|
66
|
+
| Problema | Solu????o |
|
|
67
67
|
|---|---|
|
|
68
|
-
| "Python
|
|
69
|
-
| Porta 8188 ocupada | O launcher migra para 8189
|
|
70
|
-
| `settings.json corrompido` | O sistema preserva a
|
|
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'
|
|
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`**
|
|
77
|
-
- O ProdMaker **nunca abre o navegador** (REGRA ZERO):
|
|
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
|
-
|
|
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
|
-
>
|
|
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
|
-
##
|
|
7
|
+
## Instala????o
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
# CMD/MobaXterm (Windows)
|
|
11
|
-
npx.cmd --yes --prefer-online @danhachuel/prodmaker@0.1.
|
|
12
|
-
npx.cmd --yes --prefer-online @danhachuel/prodmaker@0.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.
|
|
16
|
-
npx --yes @danhachuel/prodmaker@0.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
|
|
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**,
|
|
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
|
-
##
|
|
23
|
+
## Se o servidor fechar sozinho no Windows
|
|
24
24
|
|
|
25
|
-
|
|
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
|
|
31
|
-
| `/legal` (index, privacidade LGPD, termos) |
|
|
32
|
-
| Workflow de
|
|
33
|
-
| Frontend React + Vite + Tailwind (login + painel AdsX) |
|
|
34
|
-
| Seus Produtos (CRUD real:
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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)
|
package/app/services/database.py
CHANGED
|
@@ -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
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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()
|
package/app/services/dbengine.py
CHANGED
|
@@ -31,7 +31,8 @@ def row_to_dict(row) -> dict:
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def dsn() -> str | None:
|
|
34
|
-
|
|
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:
|