portal/, kanban/ and agente/ were 2,034 lines implementing the original Tiny-first model: token upload links, a second SQLite Kanban, a factory agent. Nothing imported or started any of it, and several endpoints took the acting user from the request body with no authentication at all. Their real cost was that a reader arriving at this repository found two Kanbans and two portals and had to work out which one was real. The root schema.sql and .env.exemplo went with them: both code paths load local/schema.sql, and having .env.exemplo beside .env.example differing by one letter was a trap rather than a convenience. The documents describing that model are archived rather than deleted. They record decisions and reasoning the current documents do not repeat, so they are worth keeping as background, with a header saying plainly that they are not instructions. README.md keeps its business case — the capacity figures and the cost argument are still the reason this project exists — but now states where the prototype documentation begins and that the code it describes is gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9.5 KiB
API — contratos
Historical prototype contracts only. The active local API is documented at localhost:8000/docs and in LOCAL_SETUP.md.
CONTEXT.mdoverrides the legacy endpoints and workflows below.
Todos os endpoints do módulo DTF, com requisição e resposta. Base:
portal/main.pyekanban/main.py.
Autenticação
| Quem chama | Como se autentica |
|---|---|
| Tiny (webhook) | header x-token com WEBHOOK_TOKEN |
| Cliente | token no path da URL — sem login |
| Agente | header x-token com AGENTE_TOKEN |
| Kanban (aba do PCP) | sessão do PCP — rede interna, sem porta para fora |
PORTAL · na nuvem
POST /webhook/tiny
Chamado quando nasce um pedido de DTF. É o gatilho de tudo.
{ "numero": "48213", "cliente": "Estamparia Vitória",
"telefone": "5516999998888", "metros": 2.4, "cliente_novo": false }
200
{ "ok": true, "link": "https://arte.dropstaratacado.com.br/arte/a7f3k9..." }
Cria o pedido, gera token de 7 dias e enfileira o WhatsApp com o link.
Se o número já existir, devolve {"ok":true,"ja_existia":true} sem duplicar.
Latência importa. O link só sai depois que o pedido existe no Tiny — se a integração VNDA → Tiny demorar, o cliente espera. Medir antes de subir.
GET /arte/{token}
Página de upload. HTML. O token já sabe o pedido — o cliente não digita nada.
GET /api/arte/{token}
{ "pedido": "48213", "cliente": "Estamparia Vitória", "metros": 2.4,
"area_util_cm": 97, "largura_max_cm": 57,
"limite_arquivo_m": 20, "max_gb": 5, "max_arquivos": 10 }
410 se o token expirou.
POST /api/arte/{token}/url
{ "nome": "estampa.png", "bytes": 84000000 }
{ "url": "https://r2.../48213/ab12_estampa.png?X-Amz-...",
"chave": "48213/ab12_estampa.png", "expira_em": 3600 }
Upload direto para o storage. Arquivo de 5 GB passando pelo VPS derrubaria o processo. Em partes, para arquivos grandes.
POST /api/arte/{token}/pronto
{ "chave": "48213/ab12_estampa.png", "repeticoes": 20 }
{ "arte_id": 991, "status": "processando" }
Dispara o pré-flight em background. O cliente acompanha por polling em
GET /api/arte/{token}/status.
GET /api/arte/{token}/status
Aprovada:
{ "status": "aprovada", "dpi_efetivo": 300, "qualidade_pct": 100,
"metros_totais": 48.0, "minutos_maquina": 144,
"partes": [{"ordem":1,"metros":20,"nome":"48213_p1.png"},
{"ordem":2,"metros":20,"nome":"48213_p2.png"},
{"ordem":3,"metros":8,"nome":"48213_p3_carimbado.png"}],
"avisos": [], "previsao_saida": "2026-09-02T18:00:00-03:00" }
Recusada:
{ "status": "recusada",
"motivo": "A resolução real da arte é de 72 DPI no tamanho que você comprou. Precisamos de pelo menos 150 DPI — o ideal é 300. Reenvie em maior resolução." }
O relógio do prazo só começa quando status = aprovada. Arquivo ruim
enviado às 17h não faz as horas correrem contra a casa.
GET /api/artes · o agente busca
Header x-token. Devolve o que está aprovado, com vírus liberado e ainda não baixado.
[{ "arte_id": 991, "pedido": "48213", "cliente": "Estamparia Vitória",
"metros": 48.0, "minutos_maquina": 144, "conferir_manual": false,
"partes": [{"ordem":1,"metros":20,"nome":"48213_p1.png",
"url":"https://r2.../...","sha256":"a3f9..."}] }]
POST /api/artes/{id}/baixada · confirma o download
POST /api/agente/heartbeat · a cada 5 min
Sem sinal por 15 minutos, alertar o TI. Serviço silencioso parado é pior que erro barulhento: ninguém percebe até o cliente cobrar.
GET /cliente/{token_cliente} · minhas artes
Link permanente do cliente, não do pedido. Lista as artes tratadas dos últimos 12 meses.
POST /cliente/{token_cliente}/reimprimir
{ "arte_id": 812, "metros": 20 }
Abre pedido novo no Tiny com a arte já pronta. Recompra sem atrito.
KANBAN · rede interna, aba do PCP
GET /api/quadro
{ "colunas": [{"id":"rec","nome":"Arte recebida"}, ...],
"cards": {
"fil": [{ "arte_id": 991, "pedido": "48213", "cliente": "Estamparia Vitória",
"metros": 48.0, "minutos_maquina": 144, "partes": 3,
"maquina": null, "desde": "2026-09-02T14:02:00",
"parado_seg": 4320, "conferir_manual": false }]
},
"maquinas": [{ "n": 1, "ocupada": true, "pedido": "48190",
"metros": 9.0, "rodando_seg": 720 },
{ "n": 3, "ocupada": false }] }
maquinas é o que pinta o círculo vermelho. Com seis máquinas vendo o mesmo
quadro, é o que evita dois operadores no mesmo arquivo.
POST /api/puxar
{ "maquina": 4, "usuario": "alexandre" }
{ "maquina": "Maq 4", "pedido": "48197", "metros": 18.0,
"minutos": 60, "reserva_expira_em": 3 }
Um pedido por vez e reserva de 3 minutos — ajustes pedidos pela sala.
Pega sempre o mais antigo da fila. Se não entrar em imp em 3 min, volta.
409 se a máquina já estiver ocupada. 404 se a fila estiver vazia.
POST /api/devolver
{ "arte_id": 991, "usuario": "thales", "motivo": "travou no meio" }
Volta ao topo da fila, não ao fim — o pedido já esperou uma vez.
POST /api/mover
{ "arte_id": 991, "para": "fin", "usuario": "poliana", "maquina": 4 }
Grava o movimento antes de qualquer integração externa. Se o Tiny estiver fora, o job fica na fila e tenta de novo em 1, 5 e 30 min — mas a medição de tempo já está salva.
Move o arquivo entre as pastas e enfileira marcador e WhatsApp quando a coluna
pedir. Ao ir para apc, pergunta o motivo da prova de cor.
PATCH /api/card/{arte_id}
{ "minutos_maquina": 90 }
Ajuste da estimativa. O sistema sugere por metros/20*60; quem trata a arte
corrige só quando foge do normal. É esse número que soma a fila contra a
capacidade do dia.
GET /p/{pedido}
O QR do carimbo aponta para cá. Redireciona para o card. A revisão bipa e cai na tela do pedido em vez de procurar na lista.
Retrabalho
POST /api/retrabalho
{ "pedido_origem": "48184", "metros": 2.0, "causa": "impressao",
"aberto_por": "mayana", "evidencia": "print-whatsapp.jpg" }
{ "id": 44, "alcada": "sala", "vez": 1, "conta_na_meta": true }
A Mayana abre e classifica — conhece o cliente e a reclamação. A causa fica travada. Quem discorda contesta, não altera.
POST /api/retrabalho/{id}/autorizar
{ "usuario": "thales" }
Thales ou Alexandre autorizam, porque o retrabalho da casa entra na meta deles. Eles confirmaram achar justo.
403 se a alçada exigir financeiro ou diretoria.
POST /api/retrabalho/{id}/contestar
{ "usuario": "alexandre", "texto": "não foi impressão, a arte veio em CMYK" }
Registra a discordância sem mudar a causa. Fica com quem pediu e quem decidiu.
Relatórios
GET /api/relatorio/etapas?dias=7
[{ "coluna": "tra", "nome": "Arte tratada", "movimentos": 84,
"media_min": 94, "pior_min": 380 }]
Responde a pergunta que decide o terceiro turno: quanto do tempo é fila e quanto é trabalho. Se a arte demora 6h e a máquina imprime em 40 min, rodar 24h só faz a fila esperar de madrugada.
GET /api/relatorio/impressao?dias=7
[{ "maquina": "Maq 1", "pedidos": 42, "media_min": 34,
"metros": 310.5, "m_por_hora": 20.4 }]
m_por_hora valida ou derruba os 20 m/h. Todo o cálculo de capacidade —
60.480 metros/mês, R$ 148 mil de ganho — depende desse número. Se for 14, a
conta muda inteira.
GET /api/relatorio/aproveitamento?dias=30
{ "metros_faturados": 11605, "metros_de_filme": 12693,
"aproveitamento_pct": 91.4, "valor_do_ponto_mes": 980,
"fonte": "transferências para o depósito Sala DTF no Tiny" }
Não exige apontamento novo. O consumo já é registrado quando o insumo é transferido para o depósito de impressão — a Altus faz isso porque também revende insumo.
GET /api/relatorio/retrabalho?dias=30
{ "por_causa": [{ "causa": "impressao", "descricao": "Falha de impressão",
"responsavel": "Thales e Alexandre", "pedidos": 9,
"metros": 24.5, "pct": 1.1, "meta": 0.4, "bate": false }],
"total_casa_pct": 2.9, "meta_casa_pct": 1.6 }
⚠ A meta ainda não está decidida. A proposta era 0,4% por causa; a sala
respondeu que "meta geral seria melhor". META_POR_CAUSA está isolado no código
para trocar sem mexer no resto.
Códigos de erro
| Código | Quando |
|---|---|
| 400 | payload inválido, coluna inexistente, mais de 10 arquivos |
| 401 | token do webhook ou do agente errado |
| 403 | alçada insuficiente para autorizar retrabalho |
| 404 | token não existe, card não encontrado, fila vazia |
| 409 | máquina já ocupada, pedido já reservado |
| 410 | token do link expirou |
| 413 | arquivo acima de 5 GB |
| 429 | rate limit — 20 req/min por token |
O que testar antes de dar por pronto
- Upload de arquivo de 5 GB sem derrubar o VPS
- Dois operadores clicando
puxarao mesmo tempo — só um pega - Reserva expirando: puxar e não mover em 3 min devolve à fila
- Tiny fora do ar: o movimento grava e o job fica na fila
- Agente sem internet: para, e ao voltar baixa o acumulado
- Arquivo baixado pela metade não aparece no kanban
- Token expirado devolve 410 com mensagem clara ao cliente