Files
dtf-system/dtf-organograma-ti.html
Cauê Faleiros 98c951d374
Some checks failed
Validate, publish and deploy / validate (push) Successful in 2m2s
Validate, publish and deploy / publish-and-deploy (push) Failing after 8s
first commit
2026-09-15 16:42:34 -03:00

206 lines
15 KiB
HTML

<!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>