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>
329 lines
9.5 KiB
Markdown
329 lines
9.5 KiB
Markdown
# API — contratos
|
|
|
|
> Historical prototype contracts only. The active local API is documented at
|
|
> [localhost:8000/docs](http://localhost:8000/docs) and in [LOCAL_SETUP.md](LOCAL_SETUP.md).
|
|
> `CONTEXT.md` overrides the legacy endpoints and workflows below.
|
|
|
|
> Todos os endpoints do módulo DTF, com requisição e resposta.
|
|
> Base: `portal/main.py` e `kanban/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.**
|
|
|
|
```json
|
|
{ "numero": "48213", "cliente": "Estamparia Vitória",
|
|
"telefone": "5516999998888", "metros": 2.4, "cliente_novo": false }
|
|
```
|
|
|
|
**200**
|
|
```json
|
|
{ "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}`
|
|
|
|
```json
|
|
{ "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`
|
|
|
|
```json
|
|
{ "nome": "estampa.png", "bytes": 84000000 }
|
|
```
|
|
```json
|
|
{ "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`
|
|
|
|
```json
|
|
{ "chave": "48213/ab12_estampa.png", "repeticoes": 20 }
|
|
```
|
|
```json
|
|
{ "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:**
|
|
```json
|
|
{ "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:**
|
|
```json
|
|
{ "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.
|
|
|
|
```json
|
|
[{ "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`
|
|
|
|
```json
|
|
{ "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`
|
|
|
|
```json
|
|
{ "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`
|
|
|
|
```json
|
|
{ "maquina": 4, "usuario": "alexandre" }
|
|
```
|
|
```json
|
|
{ "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`
|
|
|
|
```json
|
|
{ "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`
|
|
|
|
```json
|
|
{ "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}`
|
|
|
|
```json
|
|
{ "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`
|
|
|
|
```json
|
|
{ "pedido_origem": "48184", "metros": 2.0, "causa": "impressao",
|
|
"aberto_por": "mayana", "evidencia": "print-whatsapp.jpg" }
|
|
```
|
|
```json
|
|
{ "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`
|
|
|
|
```json
|
|
{ "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`
|
|
|
|
```json
|
|
{ "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`
|
|
|
|
```json
|
|
[{ "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`
|
|
|
|
```json
|
|
[{ "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`
|
|
|
|
```json
|
|
{ "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`
|
|
|
|
```json
|
|
{ "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 `puxar` ao 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
|