@danhachuel/prodmaker 0.1.9 → 0.1.11

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.11 — 2026-10-30
7
+
8
+ - **Parte 9 — Pinterest (orgânico), Logs unificado e FAQ/Documentação** — fecha o menu do AdsX.
9
+ - **Pinterest funcional:** geração de 10 ideias de pins por IA (título ≤100 chars, descrição ≤500,
10
+ texto na imagem, sugestão de imagem), board/link por produto, estados (ideia → pronto →
11
+ agendado → publicado), exportação CSV (selecionados ou todos) e agendamento via py3-pinterest
12
+ (import preguiçoso; erros honestos quando sem credenciais/sessão manual).
13
+ - **Logs unificado:** tabela com Operação, Estado, Data/Hora, Origem e Detalhes agregando
14
+ campanhas, páginas, infoprodutos, vendas, notificações e visitas — com filtros por operação,
15
+ estado e busca livre + cópia da linha.
16
+ - **FAQ & Documentação:** FAQ em accordion (login/REGRA ZERO/chaves/publicação/tracking/etc.)
17
+ + guias de referência servidos do `seed/references` (pixel-e-tuneis.md, webhooks-de-venda.md)
18
+ + link para /legal (verificação de aplicação LGPD).
19
+ - requirements.txt: + apprise, py3-pinterest (imports preguiçosos — boot preservado).
20
+ - Nova tabela `pins`. Único módulo restante como placeholder: **Contas do Google** (OAuth
21
+ multi-conta — backlog da parte de contas).
22
+ - **Conjunto AdsX completo:** Seus Produtos · AdsDashboard · AdsHosting · Infoprodutos ·
23
+ AdsPages · AdsWords · AdsCreator · AdsTracking (Visitas/Vendas/GCLID) · AdsBot · Pinterest ·
24
+ Chaves de API · Notificações · Logs · FAQ.
25
+
26
+ ## 0.1.10 — 2026-10-30
27
+
28
+ - **Parte 8 — AdsBot (Lançamento Assistido) + Dashboard real + Notificações (adsNotify).**
29
+ - **AdsBot:** wizard de 6 etapas (Produto → Página → Publicar → Campanhas → Tracking →
30
+ Conclusão) com progresso salvo no banco e retomável; marcação de etapas concluídas;
31
+ **Relatório de Lançamento** (produto + resumo da análise IA, páginas vinculadas com URLs
32
+ públicas, campanhas com CPA/orçamento e o checklist final do seed).
33
+ - **Dashboard com dados reais:** conversões, receita, comissão, visitas, produtos, páginas,
34
+ campanhas e infoprodutos; banner do lançamento em andamento ("faltam X passos" — efeito
35
+ meta); últimas vendas e últimas páginas. ROAS explicitamente ausente até haver registro
36
+ de investimento (nunca inventado).
37
+ - **Notificações (adsNotify):** toggles por evento (venda recebida, campanha publicada,
38
+ página publicada, infoproduto gerado), canais apprise por URL (Telegram entra
39
+ automaticamente quando Bot Token + Chat ID estão em Chaves de API), teste de envio e
40
+ histórico local com status/erro. Hooks disparados nos fluxos reais — falha de envio
41
+ nunca quebra a operação principal.
42
+ - Novas tabelas: `lancamentos`, `notificacoes`. Novos endpoints: `/api/lancamentos/*`,
43
+ `/api/notificacoes/*`, `/api/dashboard`.
44
+
6
45
  ## 0.1.09 — 2026-10-30
7
46
 
8
47
  - **Parte 7 — AdsWords & AdsCreator (palavras-chave e campanhas).**
@@ -0,0 +1,67 @@
1
+ """Dashboard — agregação de dados reais de todos os módulos."""
2
+ from fastapi import APIRouter
3
+
4
+ from ..services.database import get_db
5
+
6
+ router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
7
+
8
+
9
+ @router.get("")
10
+ def dashboard():
11
+ with get_db() as conn:
12
+ produtos = {
13
+ "total": conn.execute("SELECT COUNT(*) AS n FROM produtos").fetchone()["n"],
14
+ "aprovados": conn.execute("SELECT COUNT(*) AS n FROM produtos WHERE status = 'aprovado'").fetchone()["n"],
15
+ "em_analise": conn.execute("SELECT COUNT(*) AS n FROM produtos WHERE status = 'em_analise'").fetchone()["n"],
16
+ }
17
+ paginas = {
18
+ "total": conn.execute("SELECT COUNT(*) AS n FROM paginas").fetchone()["n"],
19
+ "publicadas": conn.execute("SELECT COUNT(*) AS n FROM paginas WHERE status = 'publicada'").fetchone()["n"],
20
+ "rascunho": conn.execute("SELECT COUNT(*) AS n FROM paginas WHERE status != 'publicada'").fetchone()["n"],
21
+ }
22
+ campanhas = {
23
+ "total": conn.execute("SELECT COUNT(*) AS n FROM campanhas").fetchone()["n"],
24
+ "publicadas": conn.execute("SELECT COUNT(*) AS n FROM campanhas WHERE status = 'publicada'").fetchone()["n"],
25
+ "rascunho": conn.execute("SELECT COUNT(*) AS n FROM campanhas WHERE status = 'rascunho'").fetchone()["n"],
26
+ "erro": conn.execute("SELECT COUNT(*) AS n FROM campanhas WHERE status = 'erro'").fetchone()["n"],
27
+ }
28
+ vendas_row = conn.execute(
29
+ "SELECT COUNT(*) AS n, COALESCE(SUM(valor),0) AS receita, COALESCE(SUM(comissao),0) AS comissao FROM vendas WHERE status = 'aprovada'"
30
+ ).fetchone()
31
+ infoprodutos = {"total": conn.execute("SELECT COUNT(*) AS n FROM infoprodutos").fetchone()["n"]}
32
+ tracking = {
33
+ "visitas": conn.execute("SELECT COUNT(*) AS n FROM visitas WHERE event = 'pageview'").fetchone()["n"],
34
+ "cliques": conn.execute("SELECT COUNT(*) AS n FROM visitas WHERE event = 'cta_click'").fetchone()["n"],
35
+ "conversoes_pendentes": conn.execute(
36
+ "SELECT COUNT(*) AS n FROM vendas WHERE status = 'aprovada' AND gclid != '' AND conversao_status != 'enviada'"
37
+ ).fetchone()["n"],
38
+ }
39
+ ultimas_vendas = [
40
+ {key: row[key] for key in row.keys()}
41
+ for row in conn.execute("SELECT id, produto_nome, valor, fonte, status, created_at FROM vendas ORDER BY id DESC LIMIT 5")
42
+ ]
43
+ ultimas_paginas = [
44
+ {key: row[key] for key in row.keys()}
45
+ for row in conn.execute("SELECT id, titulo, tipo, status, created_at FROM paginas ORDER BY id DESC LIMIT 5")
46
+ ]
47
+
48
+ from ..services.lancamentos import atual, progresso
49
+ lancamento_atual = atual()
50
+ progresso_atual = progresso(lancamento_atual) if lancamento_atual else None
51
+
52
+ return {
53
+ "produtos": produtos,
54
+ "paginas": paginas,
55
+ "campanhas": campanhas,
56
+ "infoprodutos": infoprodutos,
57
+ "vendas": {
58
+ "total": vendas_row["n"],
59
+ "receita": round(vendas_row["receita"], 2),
60
+ "comissao": round(vendas_row["comissao"], 2),
61
+ },
62
+ "tracking": tracking,
63
+ "ultimas_vendas": ultimas_vendas,
64
+ "ultimas_paginas": ultimas_paginas,
65
+ "lancamento": lancamento_atual,
66
+ "progresso": progresso_atual,
67
+ }
@@ -0,0 +1,34 @@
1
+ """Guias de referência (seed/references) — servidos para a página FAQ/Documentação."""
2
+ from pathlib import Path
3
+
4
+ from fastapi import APIRouter, HTTPException
5
+
6
+ router = APIRouter(prefix="/api/guias", tags=["guias"])
7
+
8
+ REFERENCES_DIR = Path(__file__).resolve().parent.parent.parent / "seed" / "references"
9
+
10
+
11
+ @router.get("")
12
+ def listar():
13
+ guias = []
14
+ if REFERENCES_DIR.exists():
15
+ for path in sorted(REFERENCES_DIR.glob("*.md")):
16
+ try:
17
+ titulo = ""
18
+ linhas = path.read_text(encoding="utf-8").splitlines()
19
+ if linhas and linhas[0].startswith("#"):
20
+ titulo = linhas[0].lstrip("#").strip()
21
+ guias.append({"arquivo": path.name, "titulo": titulo or path.stem, "chars": len(path.read_text(encoding="utf-8"))})
22
+ except Exception: # noqa: BLE001
23
+ continue
24
+ return guias
25
+
26
+
27
+ @router.get("/{arquivo}")
28
+ def ler(arquivo: str):
29
+ if "/" in arquivo or ".." in arquivo or not arquivo.endswith(".md"):
30
+ raise HTTPException(status_code=400, detail="Arquivo inválido.")
31
+ path = (REFERENCES_DIR / arquivo).resolve()
32
+ if not path.is_file() or REFERENCES_DIR.resolve() not in path.parents:
33
+ raise HTTPException(status_code=404, detail="Guia não encontrado.")
34
+ return {"arquivo": path.name, "conteudo": path.read_text(encoding="utf-8")}
@@ -88,7 +88,16 @@ def create_infoproduto(body: InfoprodutoBody):
88
88
  (provider, rel_pdf, rel_md, record_id),
89
89
  )
90
90
  row = conn.execute("SELECT * FROM infoprodutos WHERE id = ?", (record_id,)).fetchone()
91
- return row_to_dict(row)
91
+ record = row_to_dict(row)
92
+
93
+ from ..services.notify import notify_event
94
+
95
+ notify_event(
96
+ "infoproduto_gerado",
97
+ "Infoproduto gerado",
98
+ f"{record['titulo']} ({record['formato']}) — {record['provider']}",
99
+ )
100
+ return record
92
101
  except (LLMError, gamma_service.GammaError, RuntimeError) as exc:
93
102
  with get_db() as conn:
94
103
  conn.execute("DELETE FROM infoprodutos WHERE id = ?", (record_id,))
@@ -0,0 +1,81 @@
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+
4
+ from ..services import lancamentos
5
+ from ..services.llm import LLMError
6
+
7
+ router = APIRouter(prefix="/api/lancamentos", tags=["lancamentos"])
8
+
9
+
10
+ class IniciarBody(BaseModel):
11
+ produto_id: int | None = None
12
+
13
+
14
+ class EtapaBody(BaseModel):
15
+ etapa: int
16
+ dados: dict = {}
17
+
18
+
19
+ class ProdutoBody(BaseModel):
20
+ produto_id: int
21
+
22
+
23
+ @router.get("/etapas")
24
+ def etapas():
25
+ return lancamentos.ETAPAS
26
+
27
+
28
+ @router.get("/atual")
29
+ def lancamento_atual():
30
+ return lancamentos.atual()
31
+
32
+
33
+ @router.get("")
34
+ def listar():
35
+ return lancamentos.listar()
36
+
37
+
38
+ @router.post("")
39
+ def iniciar(body: IniciarBody):
40
+ return lancamentos.iniciar(body.produto_id)
41
+
42
+
43
+ @router.post("/{lancamento_id}/etapa")
44
+ def salvar_etapa(lancamento_id: int, body: EtapaBody):
45
+ try:
46
+ return lancamentos.salvar_etapa(lancamento_id, body.etapa, body.dados)
47
+ except LLMError as exc:
48
+ raise HTTPException(status_code=400, detail=str(exc))
49
+
50
+
51
+ @router.post("/{lancamento_id}/produto")
52
+ def definir_produto(lancamento_id: int, body: ProdutoBody):
53
+ try:
54
+ return lancamentos.definir_produto(lancamento_id, body.produto_id)
55
+ except LLMError as exc:
56
+ raise HTTPException(status_code=404, detail=str(exc))
57
+
58
+
59
+ @router.get("/{lancamento_id}/relatorio")
60
+ def relatorio(lancamento_id: int):
61
+ try:
62
+ return lancamentos.relatorio(lancamento_id)
63
+ except LLMError as exc:
64
+ raise HTTPException(status_code=404, detail=str(exc))
65
+
66
+
67
+ @router.get("/{lancamento_id}/progresso")
68
+ def progresso(lancamento_id: int):
69
+ try:
70
+ record = lancamentos._get_row(lancamento_id)
71
+ except LLMError as exc:
72
+ raise HTTPException(status_code=404, detail=str(exc))
73
+ return lancamentos.progresso(record)
74
+
75
+
76
+ @router.post("/{lancamento_id}/concluir")
77
+ def concluir(lancamento_id: int):
78
+ try:
79
+ return lancamentos.concluir(lancamento_id)
80
+ except LLMError as exc:
81
+ raise HTTPException(status_code=404, detail=str(exc))
@@ -0,0 +1,42 @@
1
+ """Logs unificados — visão de operações de todos os módulos."""
2
+ from fastapi import APIRouter
3
+
4
+ from ..services.database import get_db, row_to_dict
5
+
6
+ router = APIRouter(prefix="/api/logs", tags=["logs"])
7
+
8
+
9
+ def _entry(operacao: str, estado: str, data: str, origem: str, detalhe: str) -> dict:
10
+ return {"operacao": operacao, "estado": estado, "data": data, "origem": origem, "detalhe": detalhe}
11
+
12
+
13
+ @router.get("")
14
+ def logs(limite: int = 300, operacao: str = "", estado: str = ""):
15
+ entries: list[dict] = []
16
+ with get_db() as conn:
17
+ for row in conn.execute("SELECT * FROM campanhas ORDER BY id DESC LIMIT ?", (limite,)).fetchall():
18
+ record = row_to_dict(row)
19
+ estado_row = record["status"]
20
+ entries.append(_entry("campanha", estado_row, record["updated_at"], record["plataforma"], f"{record['nome']} — CPA R$ {record['cpa_alvo']} · R$ {record['orcamento_diario']}/dia" + (f" · erro: {record['erro'][:120]}" if record["erro"] else "")))
21
+ for row in conn.execute("SELECT * FROM paginas ORDER BY id DESC LIMIT ?", (limite,)).fetchall():
22
+ record = row_to_dict(row)
23
+ entries.append(_entry("pagina", record["status"], record["updated_at"], record["tipo"], f"{record['titulo']}" + (f" — {record['published_url']}" if record["published_url"] else "")))
24
+ for row in conn.execute("SELECT * FROM infoprodutos ORDER BY id DESC LIMIT ?", (limite,)).fetchall():
25
+ record = row_to_dict(row)
26
+ entries.append(_entry("infoproduto", "gerado", record["created_at"], record["provider"] or "llm", f"{record['titulo']} ({record['modo']})"))
27
+ for row in conn.execute("SELECT * FROM vendas ORDER BY id DESC LIMIT ?", (limite,)).fetchall():
28
+ record = row_to_dict(row)
29
+ entries.append(_entry("venda", record["status"], record["created_at"], record["fonte"], f"{record['produto_nome'] or '#' + str(record['id'])} — valor {record['valor']} · conversao {record['conversao_status']}" + (f" · gclid {record['gclid'][:16]}" if record["gclid"] else "")))
30
+ for row in conn.execute("SELECT * FROM notificacoes ORDER BY id DESC LIMIT ?", (limite,)).fetchall():
31
+ record = row_to_dict(row)
32
+ entries.append(_entry("notificacao", record["status"], record["created_at"], record["canal"] or "apprise", f"{record['titulo']}" + (f" — {record['erro'][:120]}" if record["erro"] else "")))
33
+ for row in conn.execute("SELECT * FROM visitas ORDER BY id DESC LIMIT ?", (limite,)).fetchall():
34
+ record = row_to_dict(row)
35
+ entries.append(_entry("visita", "ok", record["created_at"], record["ip"], f"{record['event']} {record['page']}" + (f" · gclid {record['gclid'][:16]}" if record["gclid"] else "")))
36
+
37
+ if operacao:
38
+ entries = [item for item in entries if item["operacao"] == operacao]
39
+ if estado:
40
+ entries = [item for item in entries if item["estado"] == estado]
41
+ entries.sort(key=lambda item: item["data"] or "", reverse=True)
42
+ return entries[:limite]
@@ -0,0 +1,40 @@
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+
4
+ from ..services import notify
5
+ from ..services.llm import LLMError
6
+
7
+ router = APIRouter(prefix="/api/notificacoes", tags=["notificacoes"])
8
+
9
+
10
+ class ConfigBody(BaseModel):
11
+ eventos: dict = {}
12
+ targets: list = []
13
+
14
+
15
+ class TesteBody(BaseModel):
16
+ titulo: str = "Teste ProdMaker"
17
+ mensagem: str = "Notificação de teste do ProdMaker."
18
+
19
+
20
+ @router.get("")
21
+ def config():
22
+ return notify.listar_config()
23
+
24
+
25
+ @router.put("")
26
+ def salvar(body: ConfigBody):
27
+ return notify.salvar_config(body.eventos, body.targets)
28
+
29
+
30
+ @router.get("/historico")
31
+ def historico():
32
+ return notify.historico()
33
+
34
+
35
+ @router.post("/teste")
36
+ def teste(body: TesteBody):
37
+ try:
38
+ return notify.enviar("teste", body.titulo, body.mensagem, forcar=True)
39
+ except LLMError as exc:
40
+ raise HTTPException(status_code=400, detail=str(exc))
@@ -130,6 +130,14 @@ def publicar(page_id: int):
130
130
  )
131
131
  updated = _get_or_404(page_id)
132
132
  updated["publish_result"] = result
133
+
134
+ from ..services.notify import notify_event
135
+
136
+ notify_event(
137
+ "pagina_publicada",
138
+ "Página publicada",
139
+ f"{updated['titulo']} — {result['pages_url']}",
140
+ )
133
141
  return updated
134
142
 
135
143
 
@@ -0,0 +1,80 @@
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+
4
+ from ..services import pinterest
5
+ from ..services.database import get_db, row_to_dict
6
+ from ..services.llm import LLMError
7
+
8
+ router = APIRouter(prefix="/api/pins", tags=["pinterest"])
9
+
10
+
11
+ def _produto_or_none(produto_id: int | None) -> dict:
12
+ if not produto_id:
13
+ return {"id": None, "nome": "Produto", "nicho": ""}
14
+ with get_db() as conn:
15
+ row = conn.execute("SELECT * FROM produtos WHERE id = ?", (produto_id,)).fetchone()
16
+ return row_to_dict(row) if row else {"id": produto_id, "nome": "Produto", "nicho": ""}
17
+
18
+
19
+ class GerarBody(BaseModel):
20
+ produto_id: int | None = None
21
+ board: str = ""
22
+ link: str = ""
23
+ quantidade: int = 10
24
+
25
+
26
+ class StatusBody(BaseModel):
27
+ status: str
28
+
29
+
30
+ class AgendarBody(BaseModel):
31
+ imagem_path: str = ""
32
+
33
+
34
+ class ExportBody(BaseModel):
35
+ pin_ids: list = []
36
+
37
+
38
+ @router.get("")
39
+ def listar(produto_id: int | None = None):
40
+ return pinterest.listar(produto_id)
41
+
42
+
43
+ @router.post("/gerar")
44
+ def gerar(body: GerarBody):
45
+ produto = _produto_or_none(body.produto_id)
46
+ try:
47
+ return pinterest.gerar_ideias(produto, body.board, body.link, body.quantidade)
48
+ except LLMError as exc:
49
+ raise HTTPException(status_code=400, detail=str(exc))
50
+
51
+
52
+ @router.put("/{pin_id}/status")
53
+ def status(pin_id: int, body: StatusBody):
54
+ try:
55
+ return pinterest.marcar(pin_id, body.status)
56
+ except LLMError as exc:
57
+ raise HTTPException(status_code=400, detail=str(exc))
58
+
59
+
60
+ @router.post("/{pin_id}/agendar")
61
+ def agendar(pin_id: int, body: AgendarBody):
62
+ try:
63
+ return pinterest.agendar(pin_id, body.imagem_path)
64
+ except LLMError as exc:
65
+ raise HTTPException(status_code=400, detail=str(exc))
66
+
67
+
68
+ @router.post("/export")
69
+ def exportar(body: ExportBody):
70
+ csv_text = pinterest.exportar_csv([int(item) for item in body.pin_ids])
71
+ return {"csv": csv_text}
72
+
73
+
74
+ @router.delete("/{pin_id}")
75
+ def remover(pin_id: int):
76
+ try:
77
+ pinterest.remover(pin_id)
78
+ except LLMError as exc:
79
+ raise HTTPException(status_code=404, detail=str(exc))
80
+ return {"ok": True}
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.09"
18
+ APP_VERSION = "0.1.11"
19
19
  COOKIE_NAME = "prodmaker_session"
20
20
  SESSION_DAYS = 30
21
21
  MASTER_ALIAS = "admin"
@@ -32,6 +32,12 @@ from .api import dominios as dominios_router # noqa: E402
32
32
  from .api import tracking as tracking_router # noqa: E402
33
33
  from .api import vendas as vendas_router # noqa: E402
34
34
  from .api import ads as ads_router # noqa: E402
35
+ from .api import lancamentos as lancamentos_router # noqa: E402
36
+ from .api import notificacoes as notificacoes_router # noqa: E402
37
+ from .api import dashboard as dashboard_router # noqa: E402
38
+ from .api import pins as pins_router # noqa: E402
39
+ from .api import logs as logs_router # noqa: E402
40
+ from .api import guias as guias_router # noqa: E402
35
41
 
36
42
  app.include_router(llm_router.router)
37
43
  app.include_router(infoprodutos_router.router)
@@ -40,6 +46,12 @@ app.include_router(dominios_router.router)
40
46
  app.include_router(tracking_router.router)
41
47
  app.include_router(vendas_router.router)
42
48
  app.include_router(ads_router.router)
49
+ app.include_router(lancamentos_router.router)
50
+ app.include_router(notificacoes_router.router)
51
+ app.include_router(dashboard_router.router)
52
+ app.include_router(pins_router.router)
53
+ app.include_router(logs_router.router)
54
+ app.include_router(guias_router.router)
43
55
 
44
56
  ensure_db()
45
57
 
@@ -246,7 +258,7 @@ def health():
246
258
 
247
259
  @app.get("/api/version")
248
260
  def version():
249
- return {"version": APP_VERSION, "canonical": "0.1.9"}
261
+ return {"version": APP_VERSION, "canonical": "0.1.11"}
250
262
 
251
263
 
252
264
  @app.get("/api/settings")
@@ -144,6 +144,11 @@ def marcar_publicada(campanha_id: int, resultado: dict, status: str = "publicada
144
144
  "UPDATE campanhas SET status = ?, resultado_json = ?, erro = ?, updated_at = ? WHERE id = ?",
145
145
  (status, json.dumps(resultado, ensure_ascii=False), erro[:2000], now_str(), campanha_id),
146
146
  )
147
+ if status == "publicada":
148
+ from .notify import notify_event
149
+
150
+ campanha = get(campanha_id)
151
+ notify_event("campanha_publicada", f"Campanha publicada ({campanha['plataforma']})", f"{campanha['nome']} — criada PAUSADA para revisão.")
147
152
  return get(campanha_id)
148
153
 
149
154
 
@@ -163,6 +163,51 @@ def ensure_db() -> None:
163
163
  columns = [row["name"] for row in conn.execute("PRAGMA table_info(produtos)")]
164
164
  if "analysis" not in columns:
165
165
  conn.execute("ALTER TABLE produtos ADD COLUMN analysis TEXT NOT NULL DEFAULT ''")
166
+ conn.execute(
167
+ """
168
+ CREATE TABLE IF NOT EXISTS lancamentos (
169
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
170
+ produto_id INTEGER,
171
+ etapa_atual INTEGER NOT NULL DEFAULT 1,
172
+ status TEXT NOT NULL DEFAULT 'em_andamento',
173
+ dados_json TEXT NOT NULL DEFAULT '{}',
174
+ created_at TEXT NOT NULL,
175
+ updated_at TEXT NOT NULL
176
+ )
177
+ """
178
+ )
179
+ conn.execute(
180
+ """
181
+ CREATE TABLE IF NOT EXISTS notificacoes (
182
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
183
+ event TEXT NOT NULL DEFAULT '',
184
+ titulo TEXT NOT NULL DEFAULT '',
185
+ mensagem TEXT NOT NULL DEFAULT '',
186
+ canal TEXT NOT NULL DEFAULT '',
187
+ status TEXT NOT NULL DEFAULT 'pendente',
188
+ erro TEXT NOT NULL DEFAULT '',
189
+ created_at TEXT NOT NULL
190
+ )
191
+ """
192
+ )
193
+ conn.execute(
194
+ """
195
+ CREATE TABLE IF NOT EXISTS pins (
196
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
197
+ produto_id INTEGER,
198
+ titulo TEXT NOT NULL,
199
+ descricao TEXT NOT NULL DEFAULT '',
200
+ texto_imagem TEXT NOT NULL DEFAULT '',
201
+ sugestao_imagem TEXT NOT NULL DEFAULT '',
202
+ board TEXT NOT NULL DEFAULT '',
203
+ link TEXT NOT NULL DEFAULT '',
204
+ status TEXT NOT NULL DEFAULT 'ideia',
205
+ provider TEXT NOT NULL DEFAULT '',
206
+ erro TEXT NOT NULL DEFAULT '',
207
+ created_at TEXT NOT NULL
208
+ )
209
+ """
210
+ )
166
211
 
167
212
 
168
213
  def now_str() -> str: