chore: remove the abandoned prototypes and archive what described them

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>
This commit is contained in:
Cauê Faleiros
2026-09-21 16:34:20 -03:00
parent 86b8199612
commit ca698434a2
21 changed files with 47 additions and 2292 deletions

328
docs/historico/API.md Normal file
View File

@@ -0,0 +1,328 @@
# 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

View File

@@ -0,0 +1,307 @@
# Módulo DTF no PCP — especificação de implementação
> Para o Wagner. Consolida o desenho depois das reuniões com os designers e com
> a sala de impressão em 02/09/2026.
> Complementa o `README.md` (arquitetura) e o código em `portal/`, `agente/` e
> `kanban/`.
---
## O que mudou depois das reuniões
Sete decisões vieram da equipe e **já estão refletidas no código**. Se algo no
código parecer estranho, provavelmente é uma delas.
| Item | Era | Ficou | Quem definiu |
|---|---|---|---|
| Reserva ao puxar | 15 min | **3 min** | sala |
| Puxar trabalho | até 30 min | **um pedido por vez** | sala |
| Marcadores de duração | seriam apagados | **viram campo `minutos_maquina`** | sala |
| Hot folder em rede | a confirmar | **aceita** | sala |
| Production Manager | a confirmar | **centraliza, mas o PC central precisa de mais capacidade** | sala |
| Licenças do Flexi | a confirmar | **por PC** | sala |
| Meta de retrabalho | 0,4% por causa | **rever: a sala prefere meta geral** | sala |
---
## Números que a equipe deu e que entram no cálculo
| Dado | Valor | De onde veio |
|---|---|---|
| Arquivos que chegam usáveis | **30%** | designers |
| Tempo para tratar arquivo bom | **3 minutos** | designers |
| Tempo para tratar arquivo ruim | **1h30** | designers |
| Formatos que mais chegam | **PNG e PDF** | designers |
| PDF que chega | **vetorial** | designers |
| SPOT | **sempre igual** | designers |
| Cores que sempre dão problema | azul, vermelho, castanho, laranja, verde — **secundárias** | sala |
| Prova de cor hoje | **não mandam para ninguém** | sala |
| Como o operador sabe o próximo | **notinha do pedido** | sala |
| Tempos de setup | **"não temos informação"** | sala |
**O de 30% é o mais consequente.** Sete de cada dez arquivos passam pelo
designer. Se o pré-flight barrar metade dos ruins na entrada, são ~3,5 arquivos
em 10 que deixam de consumir tempo de arte.
---
## Fases de implementação
Cada fase entrega valor sozinha. **Não espere a fase 4 para colocar algo no ar.**
### Fase 1 · Limpeza do Tiny — pode começar hoje
Não depende de nada nem de ninguém.
- [ ] Criar os marcadores `DTF-PRODUCAO`, `DTF-PRONTO` e `DTF-PROVA-COR`
- [ ] Tirar da **lista de sugestão** as variações digitadas à mão: `inicio 14:45`,
`inicio13:34`, `1hrs`, `3h`, `+30min de correcao` e as demais
- [ ] **Não apagar do histórico dos pedidos** — só da lista de opções
- [ ] **Preservar** `30 min`, `1 hora` e `3 horas`: são estimativa de máquina e
viram o campo `minutos_maquina`
### Fase 2 · Portal e robô — recebe 24h sozinho
Entrega valor sem o kanban existir.
- [ ] Contratar VPS, storage S3 e o subdomínio (ver README)
- [ ] Webhook do Tiny → cria token → dispara link no WhatsApp
- [ ] Página de upload com campo de repetição e prévia da montagem
- [ ] URL pré-assinada · upload em partes até **5 GB**
- [ ] Pré-flight (`preflight.py` pronto)
- [ ] Normalização: vetor → PDF, raster → TIF, CMYK → RGB
- [ ] Montagem empilhada · fatiamento em 20 m · carimbo com QR de 20 mm
- [ ] ClamAV em segundo plano
- [ ] Retenção de 30 dias · artes para recompra, 12 meses
### Fase 3 · Agente — o arquivo cai na pasta sozinho
- [ ] Serviço no servidor da fábrica (NSSM ou systemd)
- [ ] Webhook + polling de 60 s
- [ ] Download com verificação de hash
- [ ] Heartbeat a cada 5 min · alerta se sumir por 15
### Fase 4 · Kanban como aba do PCP
- [ ] Quadro com as 7 colunas e cronômetro por card
- [ ] Painel das 6 máquinas com **indicador vermelho de ocupada**
- [ ] `puxar próximo` — reserva de 3 min, um pedido por vez
- [ ] `devolver à fila` — volta ao topo
- [ ] Campo `minutos_maquina` no card, com sugestão automática
- [ ] Retrabalho: abertura, classificação de causa, autorização
- [ ] Painel com filtros de período
- [ ] Endpoints de relatório: etapas, impressão, aproveitamento, retrabalho
### Fase 5 · Duas semanas medindo
**Não pule.** É o que decide se vale o terceiro turno.
O que sai depois de duas semanas rodando:
- tempo real de cada etapa
- metros/hora reais por máquina
- quanto do tempo do designer é retrabalho de arquivo ruim
- retrabalho medido contra o que a sala achava que era
---
## Funcionalidades, uma a uma
### Portal do cliente
**Link com token.** Um por pedido, UUID v4, 7 dias de validade. O token já
carrega o número do pedido no Tiny — o cliente não digita nada.
**Upload direto para o storage.** URL pré-assinada, em partes, até 5 GB. O
arquivo nunca passa pelo VPS.
**Campo de repetição.** O cliente sobe uma arte e informa quantas vezes repetir.
O robô empilha. É o trabalho braçal que hoje o designer faz à mão.
**Prévia da montagem.** Mostra como a folha ficou antes de fechar o pedido.
Montagem **empilhada**, sem encaixe lado a lado — decisão do Marcus.
**Gabarito 57 × 97 cm** para download. Os 30 mm do rodapé são do carimbo.
**Resposta na hora.** Aprovado ou recusado com o motivo em português. **O relógio
do prazo só começa quando a arte é aprovada.**
### Robô de pré-flight
Ordem: **normalizar → validar → repetir → fatiar → carimbar.**
| Verificação | Limite | Ação |
|---|---|---|
| Canal de transparência | obrigatório | recusa |
| DPI efetivo na medida comprada | < 150 | recusa |
| Largura | > 57 cm | recusa |
| Formato | JPEG | recusa |
| DPI efetivo | 150 a 300 | aceita com aviso |
| Alfa parcial nas bordas | > 15% | aceita com aviso |
| CDR | — | **entra sem validar**, etiqueta "conferir" |
**DPI efetivo = pixels ÷ (cm comprados ÷ 2,54).** O metadado do arquivo mente.
**Normalização por natureza:** vetor sai PDF mantendo vetor, raster sai TIF com
alfa. CMYK vira RGB. **O original é sempre preservado** — pedido dos designers.
### Kanban
**Sete colunas:** arte recebida, arte tratada, aprovação de cor, fila,
imprimindo, correção, finalizado. **Não há coluna de aplicação** — a sala só
imprime o filme.
**Fila por ordem de chegada, sempre.**
**Puxar próximo.** Um botão por máquina. Reserva por 3 minutos; se não entrar em
Imprimindo nesse prazo, volta para a fila. **Um pedido por vez.**
**Indicador de máquina ocupada.** Círculo vermelho e botão desabilitado. Com seis
máquinas vendo o mesmo quadro, é o que evita dois operadores no mesmo arquivo.
**Devolver à fila.** Volta ao **topo**, não ao fim — o pedido já esperou uma vez.
**Cronômetro por card.** Tempo parado na coluna atual, em tempo real. Verde até
1h, amarelo até 3h, vermelho acima.
**Trilha por pedido.** Tempo em cada etapa e quanto do total foi só esperando.
### Estimativa de máquina
Campo `minutos_maquina` no card. **Herda os marcadores `30 min`, `1 hora` e
`3 horas` do Tiny**, que a sala confirmou serem estimativa.
- O sistema sugere pelo tamanho: `metros ÷ 20 × 60`
- Quem trata a arte ajusta **só quando foge do normal**
- É esse número que soma a fila contra a capacidade do dia
### Aprovação de cor
A sala confirmou que **hoje não manda prova para ninguém**, e que as cores
problemáticas são as secundárias: azul, vermelho, castanho, laranja e verde.
São cores fora do gamut CMYK — o monitor do cliente mostra o que a máquina não
reproduz.
Dispara sozinho em três casos: arquivo acima de 10 m, primeiro pedido do
cliente, ou cor detectada fora do gamut.
**Custo:** ~30 cm de filme, R$ 1,50. Uma reposição de 20 m custa R$ 99.
**Sugestão de implantação:** começar só com **cliente novo**. Os outros dois
gatilhos entram depois, senão metade dos pedidos vai esperar resposta e o prazo
morre.
### Retrabalho
SKU `CRRMP.TX.100CM`. **Mayana abre e classifica a causa; Thales ou Alexandre
autorizam** — eles confirmaram que acham justo, já que entra na meta deles.
**A causa fica travada depois de aberta.** Com o indicador atrelado a bônus,
haveria incentivo para reclassificar falha de máquina como culpa do cliente.
Quem discorda contesta, e a contestação fica registrada.
**⚠ A meta precisa ser redefinida.** A proposta era 0,4% por causa, somando 1,6%.
A sala respondeu que **"meta geral seria melhor"**. Antes de codificar, decidir
com o Marcus: meta única sobre o total da casa, ou por causa. O código tem
`META_POR_CAUSA` isolado justamente para isso.
### Integração com o Tiny
**Três marcadores, não sete.** O kanban é a fonte de verdade; o Tiny mostra o
estágio grosso para quem não abre o kanban.
| Movimento | Marcador |
|---|---|
| Imprimindo | `DTF-PRODUCAO` |
| Correção | `CORRECAO DTF` |
| Finalizado | `DTF-PRONTO` |
640 chamadas de API por dia em vez de 1.500.
### Mensagens ao cliente
| Quando | Mensagem |
|---|---|
| 1 hora sem arte | lembrete com o link · o pedido aparece como *faltando arquivo* |
| Arte recusada | motivo em português · o prazo não começou |
| Arte aprovada | qualidade em **% e DPI**, metragem, tempo estimado, horário previsto |
| Entrou em produção | aviso simples |
| Correção | motivo · **única que pede resposta** |
| Finalizado | pronto para retirada ou envio |
---
## O que ainda não pode ser codificado
### 1 · O SPOT automático
**Hipótese, não fato.** Os designers disseram que o SPOT é sempre igual; a sala
disse que "é possível criar um perfil só para isso". A documentação do Flexi 22
cita *transparency mask*, que gera o branco a partir da transparência de PNG e
TIF.
**Mas ninguém configurou ainda**, e a edição MiniTX é OEM.
O roteiro de teste está em `dtf-teste-branco-automatico.pdf`. Sete passos, uma
máquina, meia hora.
- **Se funcionar:** o arquivo vai do portal direto para a hot folder e o PC
central atende só exceções. A madrugada roda sem ninguém.
- **Se não funcionar:** o SPOT segue manual e todo pedido passa pelo PC central.
**A conta das 24 horas muda.**
### 2 · A capacidade do PC central
A sala confirmou que o Production Manager centraliza várias impressoras, **mas
que o PC central precisa de mais capacidade**. E as licenças do Flexi são **por
PC** — então centralizar o RIP exige licença lá.
Levantar antes de decidir: configuração atual do PC central, custo de uma
licença adicional, e quanto de memória o RIP consome num trabalho de 15 m.
### 3 · As regras de choke
Os designers **não responderam** os valores de choke nem a espessura mínima de
traço. Sem isso, a regra proposta — 2 px acima de 2 mm, 1 px de 1 a 2 mm,
nenhum abaixo de 1 mm — é chute informado.
**Só entra em código depois de validada com um arquivo real.**
### 4 · Metros/hora reais
Todo o cálculo de capacidade assume **20 m/h por máquina**. A sala não respondeu
o valor real, e disse que não tem informação sobre tempos.
Se o real for 14, a capacidade cai de 60.480 para 42.336 metros/mês e o plano
das 24 horas precisa ser refeito.
---
## Ressalvas honestas sobre o código
**`tiny.py` é chute informado.** Estrutura padrão de OAuth2 e endpoints v3, não
validado contra a documentação real. Está isolado de propósito: se o endpoint
for diferente, muda só ali.
**`carimbar()` precisa de teste visual.** A lógica está certa, mas posicionamento
de texto com pyvips sempre pede ajuste olhando o resultado impresso.
**O front do kanban usa dados fixos.** Precisa trocar por chamadas a
`/api/quadro` e `/api/mover`. O protótipo `dtf-kanban-treino.html` tem o layout e
o comportamento final.
**Nada instalado nos PCs da sala.** Decisão do Marcus. O navegador não abre
arquivo no FlexiPRINT — o desenho é miniatura no card mais botão que copia o
caminho, e File System Access API para mover arquivo pelo navegador.
---
## O que precisa de resposta antes da fase 4
1. Servidor da fábrica é Windows ou Linux?
2. A conta do Tiny já tem aplicação na API v3?
3. Confirmar na documentação do Tiny o endpoint de marcadores e o limite de
requisições por minuto
4. O número do WhatsApp migra para a API oficial da Meta?
5. Qual a latência da integração VNDA → Tiny? Define quando o link sai
6. Meta de retrabalho: geral ou por causa?
7. Configuração e licença do PC central, se for virar servidor de RIP

View File

@@ -0,0 +1,219 @@
# Local milestone implementation report
## Implemented
- A single `dtf-cloud` Compose project: Site, Kanban, FastAPI Portal/API,
PostgreSQL 17, private MinIO S3 storage, isolated ClamAV, and a mock integration
outbox/scanning worker. Restricted database and MinIO runtime identities are
provisioned by separate one-shot initialization jobs.
- Existing Site product UI and commercial functions retained, with a separate
local checkout bridge. Server pricing mirrors the four price ladders, all
grade discounts, assembly-inclusive rates, minimum, and rounding.
- Direct multipart browser uploads, part resumption, owned upload completion,
size verification, private five-minute operator downloads, persistent volumes,
and 30-day object/one-day incomplete-upload lifecycle rules.
- Authenticated manual quote review supplies trusted commercial quantities and
grades without adding pre-flight. Immutable server quotes include mock freight;
the customer confirms the total and creates an idempotent local paid order.
- Shared PostgreSQL orders appear in Kanban. Validated transitions, correction
reasons, version checks and movement history persist. Tiny/WhatsApp events use
a transactional outbox, retry scheduling, unique event keys, and fake receipts.
- Loopback-only published ports; API/worker/database on an isolated network;
guards reject production mode, real adapters and nonlocal storage endpoints.
All long-running services have health checks. `.env.example` contains disposable local
defaults only. No new production credentials or provider endpoints.
- Local customer registration/login, scrypt password hashes, revocable database
sessions and attempt throttling. Customers see owned orders, timelines, final
files and correction requests, and can upload corrections through the portal.
- Browser-local unfinished-cart recovery for 24 hours, including file blobs,
with storage errors surfaced. Recovered rows can be removed/replaced and new
items added. Customer navigation now points to local pages instead of the old
external account/cart destinations.
- Manual final-file sets cover every order item, support multiple parts, and
are required before queue entry. Corrections invalidate old final approvals.
Customer and operator file views distinguish originals, corrections and finals.
- Worker retention cleanup: one-day unfinished uploads, seven-day originals
after final artwork approval, and final/correction files no later than 30 days
from the first upload. Combined database/clean-object backup plus isolated,
hash-checked restore verification.
- Upload extension/size/count quotas, exact signed multipart `Content-Length`,
Host/cross-origin rejection, CSP and security headers, escaped filenames, and
logout cleanup of browser cart blobs and checkout metadata.
- Expiring, revocable HttpOnly operator sessions replace browser-stored Basic
credentials. Customer passwords use stronger scrypt parameters, with legacy
verification and upgrade on login.
- Completed artwork is quarantined until local ClamAV marks it `clean`. Quote,
payment, download, final-file approval, queue, and printing gates fail closed.
Scanner errors/rejections remain blocked and expire within three days. This is
malware scanning only, not print pre-flight.
- Structured redacted security logging, a 30-day `security_events` table, live
scanner/signature alert summaries, security regressions, exact-runtime Python
auditing, and JSON image-scan reports under `output/security/`.
- Reproducible Python 3.12 dependency resolution: all 26 direct/transitive runtime
packages are pinned with artifact hashes, and image builds enforce those hashes.
- A separate network-disabled staging-readiness gate validates non-secret inputs;
it is deliberately not an application deployment or provider connection.
- A separate production delivery package defines non-root, hash-locked API/web
images, one external-secret Portainer Docker Swarm stack, health-monitored
rolling updates, commit-SHA rollback, and one Gitea test/scan/publish/webhook
workflow matching the established Graphs/ComporHUB operating model. Its
preflight deliberately blocks the current local-only source and placeholder
inputs; no registry push, Swarm deployment, or real provider call occurred.
## URLs
| Component | URL |
|---|---|
| Site | http://localhost:8080 |
| Customer portal | http://localhost:8080/portal.html |
| Kanban | http://localhost:8081 |
| API health | http://localhost:8000/health |
| Local object API and console | http://localhost:9000 · http://localhost:9001 |
Kanban local login: `operator` / `local-operator-only`.
Setup and the complete browser test are in `LOCAL_SETUP.md`.
Interactive `/docs` and `/redoc` are disabled.
## Verification completed
- `docker compose up --build -d --wait`: all seven long-running services healthy;
both initialization jobs exited successfully and published ports are loopback-only.
- `python3 -m unittest local.test_pricing -v`: all tests passed, including
4,444 comparisons with the actual Site JavaScript calculator.
- `python3 -m local.smoke_test`: passed multipart resume/incomplete completion,
download byte identity, private bucket and session ownership, input/tamper
rejection, four-mode pricing, reviewed corrections, mock freight, concurrent
payment idempotency, valid/invalid transitions, history and eight mock receipts.
- `node local/browser_test.mjs`: passed actual browser upload, manual review,
local payment, cart recovery, final-file approval, customer registration,
customer tracking, Kanban state changes and persisted board reload, with no
JavaScript exceptions. Screenshots inspected in `output/local/`.
- Local test orders are retained in **Finalizado** for inspection, including
the browser fixture at **R$21.89** and the four-mode smoke order at **R$537.25**.
- Restarts of all long-running containers preserved order snapshots, states, mock
receipts and original file bytes; all services returned to healthy.
- Explicit guard checks rejected production mode, every real integration adapter,
R2 selection and a nonlocal S3 endpoint without contacting external services.
- `python3 -m local.workflow_test`: passed account/session isolation, guest
migration, revoked-cookie rejection, customer corrections, final revision
invalidation, secure file downloads and final-file gating.
- `python3 -m local.backup create-and-verify`: the refreshed 2026-09-15 bundle
archived 61 clean MinIO objects (58,723,323 bytes) alongside the local database,
verified SHA-256 manifests,
restored and rehashed every object under an isolated MinIO prefix, restored the
database with 16 orders and 107 upload records, then removed all temporary restore
targets. The prior database-only backup also passed the legacy verifier.
- `docker compose exec -T api python -m local.retention_test`: verified expired
object deletion, preservation of unexpired bytes, and retention of upload
metadata. Only synthetic retention-test object bytes were cleaned up.
- `python3 -m local.security_test`: passed CSP/frame/Host/origin defenses,
rejection of Basic credentials, HttpOnly operator-session revocation, extension
and multipart-size signing, upload quotas, and login throttling.
- `python3 -m local.scanning_test`: a real harmless EICAR fixture was rejected by
ClamAV and could not be downloaded or quoted; a clean control was released.
- `docker compose exec -T api python -m local.runtime_security_test`: passed
database/MinIO least privilege, legacy/current password hashes, quarantine
states, live scanner commands, scan-size rejection, and fail-closed offline behavior.
- The hash-enforced rebuild completed successfully; `pip check`, four staging-gate
regressions, security, smoke, and runtime-security tests passed. A fresh
exact-runtime `pip-audit` found no known Python advisories on 2026-09-15.
- `node local/browser_test.mjs`: additionally passed filename XSS probes with CSP
bypassed, absence of stored operator credentials, and logout removal of File blobs.
- The final rebuilt stack has seven healthy long-running services; Site/API routing and a fresh
guest portal session were checked after the rebuild.
- Production-package validation passed Python/unit and shell syntax checks, Gitea
workflow YAML parsing, and `docker stack config` rendering. The API production
image built from an immutable Python base. The web production image built from
immutable Python/Nginx bases and ran as UID 101 with a read-only filesystem,
returning 200 for its configured Host and 400 for an unexpected Host.
- The pinned ClamAV validation image started as UID 100:101 with a read-only
filesystem, all capabilities dropped, and `no-new-privileges`, then returned
`PONG` through the production health command; those restrictions are encoded in
the Swarm stack.
- `python3 deploy/production_preflight.py --source-only` returned the expected
blocked result for local-only adapters/hosts/fake providers and missing Docker
secret-file loading. This is verified fail-closed behavior, not production
acceptance.
The final closeout smoke test retained local order #14 in **Finalizado** with
eight durable fake receipts. The final workflow test again passed identity,
correction, download, invalidation, and final-file gates after the PostgreSQL 17
image refresh. Existing database and MinIO named volumes were preserved.
## Reuse and replacement decisions
| Existing asset | Decision |
|---|---|
| `dtf-site.html` | Reused directly. Preserved layout, modes, pricing, client previews, minimums and rounding; added file references and hooks for local checkout/freight. |
| `portal/main.py` | Inspected and preserved. Reused FastAPI and direct signed S3 upload design; replaced runtime with `local/app.py` because the prototype starts from Tiny and invokes pre-flight/agent delivery. |
| `portal/preflight.py` | Inspected, untouched, never imported by local runtime. |
| `kanban/main.py` | Inspected and preserved. Reused workflow state names, movement-history concept and outbox approach; replaced SQLite/folder/machine runtime with PostgreSQL endpoints. |
| `kanban/static/kanban.html` | Inspected and preserved. Reused dark palette, cards, columns and drag/drop pattern in `local/static/kanban.html`. Legacy machine controls and conflicting local/server handlers are not loaded. |
| Duplicated Tiny/WhatsApp modules | Preserved but excluded from build/runtime. New explicit fake adapters cannot invoke them. |
| `agente/` | Inspected, untouched, excluded from build and Compose. Factory automation remains out of scope. |
| Root `schema.sql`, `requirements.txt`, `.env.exemplo` | Historical only; local runtime uses a separate `dtf_local` PostgreSQL schema, dependencies, and `.env.example`. No migration of prototype data. |
| README/API docs | Legacy content preserved under explicit notices pointing to active local instructions. |
## Deferred and practical limits
This is a working local development milestone, not the completed three-week production
MVP. Real payments/webhooks, freight providers, Tiny/Olist, WhatsApp, production
R2, production account verification/recovery/security, automatic final print-file
generation, scheduled/offsite backups and a production restore runbook, and production
activation remain undone. Deployment definitions and automation now exist, but
their gate correctly prevents use with the local-only application. No factory agent, hot folders, FlexiPRINT, VPN,
machine dashboards, label automation, or new automatic pre-flight were added.
Browser artwork analysis already in the Site is retained as advisory prototype
behavior. The backend never adopts its prices, lengths or grades as approved.
The temporary manual quote step resolves that trust boundary locally; its role
in production needs an explicit product decision. Final files are manually
prepared and approved by operators; the test `.cdr` fixture is text, not printable
artwork. Cart recovery is browser-local, subject to quota, and does not reconstruct
the active artwork editor in place. Fake delivery receipts
mean only that the local adapter recorded an event, never that a person received
a message or an ERP order was created.
The multipart transport supports up to 5 GiB, but this local scanner can release
only files up to 128 MiB. Larger uploads remain blocked. The scanner has no
external network route and uses signatures bundled in its pinned image; rebuild
or replace that image before signatures exceed the seven-day alert threshold.
The 2026-09-15 exact-runtime Python audit reported no known dependency findings.
All 26 installed packages are transitively pinned with artifact hashes; scheduled
lock refresh and audit automation are still pending.
Trivy HIGH/CRITICAL image reports are retained in `output/security/`: API 46
(44 Debian records without fixes plus two third-party-SBOM package records absent
from the runtime), Site 5, refreshed PostgreSQL 31, pinned MinIO 108, and pinned
ClamAV 0. Counts are scanner observations, not exploitability determinations.
MinIO was not upgraded over the preserved object volume; its old pinned release
is a material localhost-only limitation. See `SECURITY_REPORT.md`.
The Site retains its pre-existing external fonts/logo/PDF.js 3.11.174 references.
The known eval advisory is mitigated by the existing `isEvalSupported:false` call;
the old CDN dependency still needs a planned upgrade or vendored/pinned
replacement. No new production service references were added. The `.git` directory is unavailable
as a working Git repository in this workspace, so changes are delivered as local
files without a commit or Git diff.
## Exact next step
The localhost security closeout is complete; continue local product work without
treating these controls as production approval. Before any staging or production
connection, complete `PRODUCTION_INPUTS.md` and pass the network-disabled readiness
gate, including the acceptance flow,
production authority for length/grade, and freight platform, origin, packaging and
policy. The verified local database/clean-object bundle addresses the local
recovery gap but is neither scheduled nor offsite and should be created while local
writes are idle. Remaining foundations include production backup/restore design,
container-image remediation/upgrade decisions, and account verification/recovery
design. The current `compose.staging.yaml` validates inputs only and cannot deploy
the application or contact providers.
Use the checked-in `deploy/` package as the target deployment contract rather
than creating another production stack. First add the actual separate staging application runtime, beginning with S3
adapter contract tests for direct multipart upload, private downloads, CORS and
retention using injected staging credentials. Add real provider adapters only
after their contracts are confirmed; payment activation requires signed,
idempotent webhooks. Implement Docker-secret file loading, resolve or formally
accept image findings, rehearse production restore, and make the release preflight
pass without weakening it. The local runtime intentionally refuses production values.

26
docs/historico/README.md Normal file
View File

@@ -0,0 +1,26 @@
# Historical documents
These describe the project as it was originally designed: a Tiny-first flow with
token upload links, a factory-side agent, a local SQLite Kanban, FlexiPRINT
automation, 90-day retention and 12-month artwork reuse.
**None of it describes the system that exists.** `CONTEXT.md` is the current
source of truth and `ROADMAP.md` tracks outstanding work. These are kept because
they record decisions and reasoning that the current documents do not repeat —
read them for background, never as instructions.
| File | What it was |
|---|---|
| `ESPECIFICACAO.md` | Original specification of the Tiny-first model |
| `API.md` | Endpoint sketch for the prototype portal |
| `TAREFAS.md` | Task plan written before the current scope |
| `IMPLEMENTATION_REPORT.md` | Status report for the prototype milestone |
| `dtf-organograma-*` | Organisation charts, referenced by nothing |
The prototype code they describe — `portal/`, `kanban/`, `agente/`, the root
`schema.sql` and `.env.exemplo` — was removed once nothing referenced it. Recover
it from Git history if it is ever needed:
```bash
git log --oneline --diff-filter=D -- portal kanban agente
```

212
docs/historico/TAREFAS.md Normal file
View File

@@ -0,0 +1,212 @@
# Tarefas — em ordem de execução
> Cada tarefa tem **critério de aceite**: o que precisa acontecer para ela ser
> considerada pronta. Sem isso, "terminei" vira discussão.
>
> A ordem importa: as três primeiras não dependem de decisão de ninguém.
---
## Bloco A · Pode começar hoje
Nada aqui depende de resposta pendente.
### A1 · Limpar os marcadores do Tiny
**Aceite:** a lista de sugestão de marcadores só mostra os padrões. Os pedidos
antigos mantêm o histórico intacto.
- Criar `DTF-PRODUCAO`, `DTF-PRONTO`, `DTF-PROVA-COR`
- Remover da lista de sugestão: `inicio 14:45`, `inicio13:34`, `1hrs`, `3h`,
`+30min de correcao` e as demais variações digitadas à mão
- **Não apagar do histórico dos pedidos** — só da lista de opções
- **Preservar** `30 min`, `1 hora`, `3 horas`: são estimativa de máquina
### A2 · Contratar a infraestrutura
**Aceite:** `https://arte.dropstaratacado.com.br` responde com certificado
válido, e um arquivo de teste sobe e desce do storage.
- VPS Linux 2 vCPU / 4 GB · Hetzner, Contabo ou Hostinger
- Storage S3 compatível · Cloudflare R2 ou Backblaze B2
- Subdomínio + Let's Encrypt
- Bucket `dtf-artes` com ciclo de vida de 30 dias
- Prefixo `artes-cliente/` com 12 meses
- CORS liberado só para o domínio do portal
### A3 · Criar o banco
**Aceite:** `schema.sql` roda sem erro no Postgres e no SQLite.
- Postgres na nuvem · SQLite na fábrica
- Dump diário do Postgres para o storage
### A4 · Provisionar acessos
**Aceite:** um token do Tiny obtido por código, e um WhatsApp de teste enviado.
- Aplicação na API v3 do Tiny · client id e secret
- **Confirmar na documentação** o endpoint de marcadores e o limite de req/min
- WhatsApp: definir Meta oficial ou provedor
---
## Bloco B · Portal — entrega sozinho
Ao fim deste bloco a Altus **já recebe arte 24 horas**, mesmo sem kanban.
### B1 · Webhook e link
**Aceite:** um pedido criado no Tiny gera link e chega no WhatsApp em menos de
1 minuto.
- `POST /webhook/tiny` → cria pedido, token de 7 dias, dispara WhatsApp
- Idempotente: pedido repetido não duplica
- **Medir a latência VNDA → Tiny** e registrar
### B2 · Página de upload
**Aceite:** um arquivo de 5 GB sobe sem derrubar o VPS.
- URL pré-assinada, upload em partes
- Campo de repetição com prévia da conta
- Gabarito 57 × 97 cm para download
- Rate limit de 20 req/min por token
### B3 · Pré-flight
**Aceite:** dos cinco arquivos que os designers separarem, o robô decide igual
ao que eles decidiriam.
- `preflight.py` está pronto — integrar e testar
- Normalização: vetor → PDF, raster → TIF, CMYK → RGB, **original preservado**
- Repetição, fatiamento em 20 m, carimbo com QR de 20 mm
- CDR entra sem validar, etiqueta "conferir"
- **`carimbar()` precisa de teste visual impresso** antes de valer
### B4 · Mensagens
**Aceite:** as seis mensagens chegam com o texto certo, e a de correção pede
resposta.
- Lembrete de 1 hora sem arte
- Recusa com motivo em português
- Aprovação com **% e DPI**, metragem, tempo e horário previsto
- Em produção, correção, finalizado
### B5 · Antivírus
**Aceite:** um EICAR de teste é barrado e não chega ao agente.
- ClamAV em segundo plano, sem travar a fila
---
## Bloco C · Agente
### C1 · Serviço na fábrica
**Aceite:** derrubar a internet por 10 minutos e religar — o agente baixa o
acumulado sozinho, sem duplicar nada.
- Windows via NSSM ou Linux via systemd
- Webhook + polling de 60 s
- Download em arquivo temporário, renomeia só quando completa
- Verificação de SHA-256
- Idempotente pelo `arte_id`
### C2 · Monitoramento
**Aceite:** matar o processo dispara alerta em até 15 minutos.
- Heartbeat a cada 5 min
- Alerta ao TI se sumir
---
## Bloco D · Kanban como aba do PCP
### D1 · Quadro
**Aceite:** dois operadores clicando `puxar` ao mesmo tempo — só um pega.
- 7 colunas · **sem coluna de aplicação**
- Cronômetro por card, em tempo real
- Front pronto em `kanban/static/kanban.html`
- `?maq=4` abre filtrado na máquina
### D2 · Puxar e devolver
**Aceite:** puxar e não mover em 3 minutos devolve o pedido à fila sozinho.
- Reserva de **3 min** · **um pedido por vez**
- Devolver volta ao **topo**
- Círculo vermelho na máquina ocupada, botão desabilitado
### D3 · Movimento e integração
**Aceite:** desligar o Tiny, mover cards, religar — os marcadores entram sozinhos
e nenhum movimento se perde.
- Gravar o movimento **antes** de qualquer chamada externa
- Fila de jobs com retentativa em 1, 5 e 30 min
- Três marcadores no Tiny, não sete
- Arquivo acompanha o card entre as pastas
### D4 · Estimativa de máquina
**Aceite:** o card sugere os minutos e aceita ajuste manual.
- Campo `minutos_maquina`, sugerido por `metros/20*60`
- Importar os marcadores `30 min`, `1 hora`, `3 horas` dos pedidos existentes
### D5 · Retrabalho
**Aceite:** a causa não pode ser alterada depois de aberta, nem pelo autorizador.
- Abertura com causa obrigatória e evidência
- Alçada automática · reincidência sobe nível
- Contestação registrada sem alterar a causa
- **Meta ainda não definida** — `META_POR_CAUSA` isolado
### D6 · Painel e relatórios
**Aceite:** os quatro relatórios respondem com dados reais depois de uma semana.
- Filtros de período
- `/api/relatorio/etapas`, `/impressao`, `/aproveitamento`, `/retrabalho`
- Aproveitamento lendo as transferências do depósito no Tiny
---
## Bloco E · Só depois de medir
**Duas semanas com o kanban rodando antes de tocar nisto.**
### E1 · Decidir o terceiro turno
Depende de: metros/hora reais, tempo entre etapas, e quanto do tempo do designer
é retrabalho de arquivo ruim.
### E2 · SPOT automático
Depende do teste em `dtf-teste-branco-automatico.pdf`.
### E3 · PC central como servidor de RIP
Depende de: configuração atual, custo da licença (é **por PC**), e memória que o
RIP consome.
---
## Bloqueios · o que trava qual tarefa
| Tarefa | Espera por | De quem |
|---|---|---|
| B3 · pré-flight | os cinco arquivos de exemplo | designers |
| B3 · regras de choke | valor do choke e espessura mínima | designers |
| D5 · meta | meta geral ou por causa? | Marcus |
| E1 · terceiro turno | metros/hora reais | sala |
| E2 · SPOT | resultado do teste | sala |
| E3 · RIP central | config e licença do PC central | Wagner |
**Nada no bloco A, B1, B2, C e D1 a D4 está bloqueado.** Dá para chegar até o
kanban funcionando sem nenhuma dessas respostas.
---
## Ordem sugerida de trabalho
```
semana 1 A1 · A2 · A3 · A4
semana 2 B1 · B2
semana 3 B3 · B4 · B5 ← portal no ar, recebendo 24h
semana 4 C1 · C2 ← arquivo caindo na pasta sozinho
semana 5 D1 · D2
semana 6 D3 · D4
semana 7 D5 · D6 ← kanban completo
semana 8+ medir · depois E
```
**Ao fim da semana 3 a Altus já recebe arte de madrugada e recusa arquivo ruim
automaticamente**, sem nada ter mudado na sala. É o primeiro ganho real.

Binary file not shown.

View File

@@ -0,0 +1,205 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DTF — arquitetura para o TI</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{--bg:#0E1013;--card:#191C21;--card2:#1F232A;--linha:#2C3039;--tx:#E9E7E2;--fraco:#8A8F98;
--ciano:#00A6C4;--roxo:#7C6BD1;--magenta:#C8256B;--verde:#3E9B62;--alerta:#C9531F;--amarelo:#D9A400}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--tx);font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:14px;line-height:1.55;-webkit-font-smoothing:antialiased}
h1,h2,h3{font-family:"Space Grotesk",system-ui,sans-serif;letter-spacing:-.02em}
.faixa{height:5px;background:linear-gradient(90deg,var(--ciano) 0 34%,var(--roxo) 34% 67%,var(--magenta) 67% 100%)}
.wrap{max-width:1180px;margin:0 auto;padding:0 20px 70px}
header{padding:34px 0 18px}
.eb{font-size:10.5px;letter-spacing:.26em;text-transform:uppercase;color:var(--ciano);margin-bottom:10px}
h1{font-weight:700;font-size:clamp(26px,4.6vw,44px);line-height:1.03}
h1 span{color:var(--fraco)}
.lead{color:var(--fraco);max-width:70ch;margin-top:10px}
.mapa{margin:24px 0 0;overflow-x:auto}
svg{width:100%;min-width:960px;height:auto}
.zona{fill:#14161A;stroke:var(--linha);stroke-dasharray:4 3}
.zt{font-family:"IBM Plex Mono",monospace;font-size:10px;fill:var(--fraco);letter-spacing:.2em;text-transform:uppercase}
.no rect{fill:var(--card);stroke:var(--linha);cursor:pointer;transition:stroke .15s,fill .15s}
.no:hover rect{stroke:var(--ciano)}
.no[aria-current="true"] rect{fill:var(--card2);stroke-width:2}
.no text{pointer-events:none}
.nn{font-family:"Space Grotesk",sans-serif;font-size:13.5px;font-weight:700;fill:var(--tx)}
.nq{font-family:"IBM Plex Mono",monospace;font-size:9.5px;fill:var(--fraco)}
.lig{stroke:var(--linha);fill:none;marker-end:url(#s)}
.lb{font-family:"IBM Plex Mono",monospace;font-size:9.5px;fill:var(--fraco)}
.det{margin-top:26px;background:var(--card);border:1px solid var(--linha);border-left:3px solid var(--acc,var(--ciano));border-radius:9px;padding:22px 24px}
.det h2{font-size:22px;margin-bottom:2px}
.det .sub{color:var(--acc,var(--ciano));font-size:12.5px;margin-bottom:16px}
.det p{margin-bottom:12px;max-width:74ch}
pre{background:var(--card2);border:1px solid var(--linha);border-radius:7px;padding:13px 15px;overflow-x:auto;font-size:12.5px;line-height:1.6;margin:12px 0;color:#C9CDD4}
pre b{color:var(--ciano);font-weight:500} pre i{color:var(--fraco);font-style:normal} pre u{color:var(--amarelo);text-decoration:none}
.blocos{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:12px;margin-top:14px}
.b{background:var(--card2);border:1px solid var(--linha);border-radius:7px;padding:13px 15px}
.b h3{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--fraco);margin-bottom:6px;font-family:"IBM Plex Mono",monospace}
.b ul{list-style:none} .b li{padding:3px 0 3px 15px;position:relative;font-size:13px}
.b li::before{content:"·";position:absolute;left:4px;color:var(--acc,var(--ciano));font-weight:700}
.aten{margin-top:14px;border-left:2px solid var(--alerta);background:#22150F;padding:12px 16px;border-radius:0 6px 6px 0;font-size:13.5px}
.aten b{color:var(--alerta);display:block;margin-bottom:3px;font-family:"Space Grotesk",sans-serif}
code{background:var(--card2);border:1px solid var(--linha);border-radius:4px;padding:1px 6px;font-size:12.5px;color:var(--ciano)}
.dica{color:var(--fraco);font-size:12px;margin-top:10px}
@media(prefers-reduced-motion:reduce){*{transition:none!important}}
</style>
</head>
<body>
<div class="faixa"></div>
<div class="wrap">
<header>
<div class="eb">Documento técnico · Wagner · agosto 2026</div>
<h1>Arquitetura do DTF 24h<br><span>três zonas, oito componentes</span></h1>
<p class="lead">Clique em qualquer caixa para ver o que ela faz, em que linguagem, o que acontece se cair e o que ainda depende de resposta. O código completo está em <code>dtf-sistema.zip</code>.</p>
</header>
<div class="mapa">
<svg viewBox="0 0 1120 380" role="img" aria-label="Arquitetura do sistema DTF">
<defs><marker id="s" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto">
<path d="M1 1L9 5L1 9" fill="none" stroke="#2C3039" stroke-width="1.6"/></marker></defs>
<rect class="zona" x="10" y="26" width="330" height="120" rx="8"/>
<text class="zt" x="26" y="46">Terceiros</text>
<rect class="zona" x="356" y="26" width="380" height="230" rx="8"/>
<text class="zt" x="372" y="46">Nuvem · VPS + storage</text>
<rect class="zona" x="752" y="26" width="358" height="330" rx="8"/>
<text class="zt" x="768" y="46">Fábrica · rede interna</text>
<path class="lig" d="M200 84 H424"/><text class="lb" x="250" y="78">webhook</text>
<path class="lig" d="M584 110 V140"/>
<path class="lig" d="M584 196 V226"/>
<path class="lig" d="M700 168 H850"/><text class="lb" x="730" y="162">HTTPS</text>
<path class="lig" d="M916 200 V232"/>
<path class="lig" d="M916 288 V318"/>
<path class="lig" d="M850 262 H620 M620 262 V88 M620 88 H460" style="stroke-dasharray:3 3"/>
<text class="lb" x="636" y="180">marcador</text>
<path class="lig" d="M424 116 H200" style="stroke-dasharray:3 3"/><text class="lb" x="252" y="132">WhatsApp</text>
<g class="no" data-i="0" tabindex="0"><rect x="26" y="58" width="174" height="52" rx="8"/>
<text class="nn" x="42" y="82">Tiny · Olist</text><text class="nq" x="42" y="98">pedido nasce aqui</text></g>
<g class="no" data-i="1" tabindex="0"><rect x="26" y="116" width="174" height="24" rx="6"/>
<text class="nn" x="42" y="133" style="font-size:12px">WhatsApp API</text></g>
<g class="no" data-i="2" tabindex="0"><rect x="424" y="58" width="260" height="52" rx="8" style="stroke:#00A6C4"/>
<text class="nn" x="442" y="82">Portal · FastAPI</text><text class="nq" x="442" y="98">token, link, pré-assinada</text></g>
<g class="no" data-i="3" tabindex="0"><rect x="424" y="140" width="260" height="56" rx="8" style="stroke:#00A6C4"/>
<text class="nn" x="442" y="164">Robô · pyvips</text><text class="nq" x="442" y="182">valida, repete, fatia, carimba</text></g>
<g class="no" data-i="4" tabindex="0"><rect x="424" y="226" width="124" height="24" rx="6"/>
<text class="nn" x="440" y="243" style="font-size:12px">PostgreSQL</text></g>
<g class="no" data-i="5" tabindex="0"><rect x="560" y="226" width="124" height="24" rx="6"/>
<text class="nn" x="576" y="243" style="font-size:12px">Storage S3</text></g>
<g class="no" data-i="6" tabindex="0"><rect x="850" y="142" width="200" height="52" rx="8" style="stroke:#7C6BD1"/>
<text class="nn" x="866" y="166">Agente</text><text class="nq" x="866" y="182">baixa e grava na pasta</text></g>
<g class="no" data-i="7" tabindex="0"><rect x="850" y="232" width="200" height="52" rx="8"/>
<text class="nn" x="866" y="256">Pastas do servidor</text><text class="nq" x="866" y="272">00 … 60 · espelho</text></g>
<g class="no" data-i="8" tabindex="0"><rect x="850" y="318" width="200" height="52" rx="8" style="stroke:#C8256B"/>
<text class="nn" x="866" y="342">Kanban · FastAPI</text><text class="nq" x="866" y="358">SQLite · fonte de verdade</text></g>
</svg>
</div>
<div class="det" id="det"></div>
<p class="dica">Setas do teclado navegam entre os componentes.</p>
</div>
<script>
const C=[
{t:"Tiny · Olist",s:"Sistema que já existe · fonte do número do pedido",acc:"var(--fraco)",
p:["O número do Tiny é a chave de tudo. O portal só cria o link depois que o pedido existe lá.","<b>Gatilho é webhook, não polling.</b> O link sai no segundo em que o pedido nasce."],
code:`<b>POST</b> /webhook/tiny <i>header: x-token</i>
{ "numero":"48213", "cliente":"...", "telefone":"...", "metros":2.4 }`,
blocos:[["Marcadores que o sistema escreve",["DTF-PRODUCAO — entrou na máquina","CORRECAO DTF — voltou","DTF-PRONTO — finalizado"]],
["Marcadores que ele preserva",["multiempresa, VALE, Troca","BOLETO NEXSTAR SICCOB","Maq 1 a 6 (histórico)"]]],
aten:["Confirmar antes de subir","Endpoint exato de marcadores na API v3, limite de requisições por minuto e se o refresh token expira. Tudo isolado em <code>tiny.py</code> — se mudar, muda só ali."]},
{t:"WhatsApp API",s:"Meta oficial ou provedor · três avisos ao cliente",acc:"var(--fraco)",
p:["Aprovada, em produção e finalizada. Mais correção, que é a única que pede resposta. Sete avisos viraria spam e o cliente pararia de ler."],
blocos:[["Meta oficial",["Sem risco de bloqueio do número","Exige template aprovado","R$ 100 a 300/mês"]],
["Z-API ou Evolution",["Sobe rápido, sem template","Risco de bloqueio","Mais barato"]]],
aten:["Decisão pendente","O número do DTF migra para a API oficial ou fica com provedor? Muda o custo e o risco."]},
{t:"Portal · FastAPI",s:"Python 3.12 · VPS Linux · aberto na internet",acc:"var(--ciano)",
p:["Cria o token do link, entrega a URL pré-assinada e recebe o aviso de que o upload terminou.","<b>Fica fora da fábrica de propósito.</b> Se a internet da Altus cair às 3h da manhã, o cliente sobe do mesmo jeito e o agente baixa quando voltar."],
code:`<b>GET</b> /arte/{token} <i>página</i>
<b>POST</b> /api/arte/{token}/url <i>URL pré-assinada</i>
<b>POST</b> /api/arte/{token}/pronto <i>dispara o robô</i>
<b>GET</b> /api/artes <i>o agente busca aqui</i>`,
blocos:[["Por que pré-assinada",["200 MB pelo VPS derrubaria o processo","O navegador fala direto com o storage","Validade de 1 hora"]],
["Token do link",["UUID v4, 7 dias","Um por pedido, nunca reaproveitado","Rate limit de 20 req/min"]]]},
{t:"Robô · pyvips",s:"Pré-flight, repetição, fatiamento e carimbo",acc:"var(--ciano)",
p:["pyvips lê PNG e TIFF de 200 MB em streaming, sem carregar na memória. É o que permite rodar num VPS de 4 GB.","<b>A regra que mais pega arquivo ruim é o DPI efetivo.</b> O valor gravado no cabeçalho mente — o cliente escala a imagem e o programa mantém 300."],
code:`dpi = img.height / (cm_uteis / 2.54) <i># não confie no metadado</i>
<b>se</b> dpi &lt; 150: recusa
<b>se</b> repeticoes &gt; 1: empilhar()
partes = fatiar(limite_m=<u>15</u>)
carimbar(partes[-1]) <i># só na última</i>`,
blocos:[["Recusa",["Sem canal alfa","DPI efetivo abaixo de 150","Largura acima de 57 cm","JPEG ou arquivo corrompido"]],
["Aceita com aviso",["DPI entre 150 e 300","Traço abaixo de 0,4 mm","Bordas com alfa parcial"]]],
aten:["carimbar() precisa de teste visual","A lógica está certa, mas posicionamento de texto com pyvips sempre pede ajuste olhando o resultado impresso."]},
{t:"PostgreSQL",s:"Na nuvem · pedidos, artes, partes e jobs",acc:"var(--ciano)",
p:["Quatro tabelas: <code>pedido</code>, <code>arte</code>, <code>parte</code> e <code>job</code>.","<b>A fila de jobs é tabela, não broker.</b> <code>SELECT … FOR UPDATE SKIP LOCKED</code> resolve. Redis e Celery são complexidade sem retorno neste volume."],
blocos:[["Retentativa",["1 min, 5 min, 30 min","Depois desiste e alerta","O log local grava independente"]],
["Backup",["Dump diário para o storage","SQLite local no backup do servidor"]]]},
{t:"Storage S3",s:"Cloudflare R2 ou Backblaze B2",acc:"var(--ciano)",
p:["Escolhido por não cobrar saída de dados — o agente baixa tudo que sobe, e isso sairia caro em provedor que tarifa egress.","<b>Retenção de 90 dias.</b> A arte é propriedade do cliente. Regra de ciclo de vida no bucket faz a limpeza sozinha."],
aten:["Antivírus antes de liberar","ClamAV no arquivo recebido antes de o agente baixar. É arquivo de terceiro entrando na rede."]},
{t:"Agente",s:"Python · serviço no servidor da fábrica",acc:"var(--roxo)",
p:["Busca o que foi aprovado e grava em <code>00_ARTE_RECEBIDA</code>. Baixa em arquivo temporário e só renomeia quando completa — o kanban nunca vê arquivo pela metade.","Trata queda de internet como situação normal, não como erro. Para, e volta quando a conexão voltar."],
code:`webhook do portal + polling de 60 s <i>(rede de segurança)</i>
baixa → confere SHA-256 → insere no SQLite → marca baixada
heartbeat a cada 5 min`,
blocos:[["Windows",["nssm install DtfAgente","Variáveis via AppEnvironmentExtra"]],
["Linux",["dtf-agente.service","Restart=always"]]],
aten:["Serviço silencioso parado é pior que erro barulhento","Sem heartbeat por 15 minutos, o portal precisa alertar o TI. Ninguém percebe um agente morto até o cliente cobrar."]},
{t:"Pastas do servidor",s:"Espelho, não fonte de verdade",acc:"var(--fraco)",
p:["Nove pastas numeradas para ordenar sozinhas no explorador. O arquivo acompanha o card.","<b>Se a pasta e o banco divergirem, o banco ganha.</b> Um watchdog opcional captura quem move arquivo direto pelo Explorer e corrige o banco."],
code:`00_ARTE_RECEBIDA 10_ARTE_TRATADA 20_FILA_IMPRESSAO
30_IMPRIMINDO 40_CORRECAO 50_APLICACAO
60_FINALIZADO _ERRO _log`},
{t:"Kanban · FastAPI",s:"SQLite local · rede interna, sem porta para fora",acc:"var(--magenta)",
p:["Mover o card grava o movimento, move o arquivo e enfileira o marcador e o WhatsApp. Ninguém abre o Tiny para atualizar nada.","<b>O movimento é gravado antes de qualquer integração externa.</b> Se o Tiny estiver fora do ar, a medição de tempo já está salva."],
code:`<b>POST</b> /api/mover {arte_id, para, usuario, maquina}
<b>GET</b> /api/quadro <i>cards com tempo parado em tempo real</i>
<b>GET</b> /p/{pedido} <i>o QR do carimbo cai aqui</i>
<b>GET</b> /api/relatorio/etapas`,
blocos:[["Tabela movimento",["de, para, entrou, saiu, segundos","usuário e máquina","é dela que saem todos os números"]],
["O que ela responde",["Tempo médio por etapa","Fila por máquina","Taxa de retrabalho","Se o gargalo é máquina ou designer"]]],
aten:["O front ainda não está conectado","O protótipo usa dados fixos. Precisa trocar por chamadas a /api/quadro e /api/mover. Meia hora de trabalho."]}
];
const nos=[...document.querySelectorAll('.no')], det=document.getElementById('det');
function ver(i){
const c=C[i];
det.style.setProperty('--acc',c.acc);
let h=`<h2>${c.t}</h2><div class="sub">${c.s}</div>`+c.p.map(p=>`<p>${p}</p>`).join('');
if(c.code) h+=`<pre>${c.code}</pre>`;
if(c.blocos) h+='<div class="blocos">'+c.blocos.map(b=>
`<div class="b"><h3>${b[0]}</h3><ul>${b[1].map(x=>`<li>${x}</li>`).join('')}</ul></div>`).join('')+'</div>';
if(c.aten) h+=`<div class="aten"><b>${c.aten[0]}</b>${c.aten[1]}</div>`;
det.innerHTML=h;
nos.forEach((n,j)=>n.setAttribute('aria-current',j===i?'true':'false'));
}
nos.forEach(n=>{
n.addEventListener('click',()=>ver(+n.dataset.i));
n.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){ver(+n.dataset.i);e.preventDefault();}});
});
document.addEventListener('keydown',e=>{
const at=nos.findIndex(n=>n.getAttribute('aria-current')==='true');
if(e.key==='ArrowRight'&&at<nos.length-1){nos[at+1].focus();ver(at+1);e.preventDefault();}
if(e.key==='ArrowLeft'&&at>0){nos[at-1].focus();ver(at-1);e.preventDefault();}
});
ver(0);
</script>
</body>
</html>