# 3 — Arquitetura ## Visão geral ``` navegador (equipe) ──┐ ├──▶ PHP 8 no Hostinger ──▶ MySQL 8 navegador (cliente) ──┘ │ ├──▶ /storage/ originais + pirâmides (fora do web root) └──▶ SMTP externo (Brevo/Resend) máquina do estúdio ──▶ upload (multipart em pedaços) ──▶ fila ──▶ worker por cron ``` ## Layout de diretórios no servidor ``` /home/USER/ public_html/ web root index.php front controller assets/ css, js, fontes r/ página do cliente (token na URL) app/ código fora do web root Config/ Http/ Domain/ Storage/ storage/ NUNCA servido direto pelo Apache estudio_/ projeto_/ imagem_/ v1/original.png v1/tiles/ ou v1/tiles.pack + tiles.idx.json ``` Nada em `storage/` é acessível por URL. Todo byte de imagem sai por um endpoint PHP que valida sessão ou token antes de responder. É isso que impede alguém adivinhar caminho e baixar render de outro estúdio. ## Pipeline de imagem 1. **Entrada.** Upload em pedaços de 8 MB. O endpoint só concatena e grava; não abre a imagem. Registra `versoes` com estado `recebida`. 2. **Fila.** Cron de minuto em minuto pega o mais antigo em `recebida` e marca `processando`. 3. **Normalização.** Crava sRGB IEC61966-2.1, remove alfa opaco, corrige orientação EXIF. Alfa com variação real vira aviso em `versoes.avisos`, não erro. 4. **Pirâmide.** Tiles de 512 px, WebP q95, halvings sucessivos até caber num tile. 5. **Pronta.** Estado `pronta`, dimensões e caminhos gravados. Se o processamento em PHP compartilhado se mostrar instável com arquivo muito grande, a alternativa já validada é rodar os passos 3 e 4 na máquina do estúdio e enviar a pirâmide pronta. `scripts/` tem os dois utilitários funcionando. ## Servir tiles `GET /t/{versao}/{nivel}/{coluna}_{linha}.webp` Valida acesso, resolve o caminho, responde com `Cache-Control: private, max-age=31536000, immutable` e `ETag`. Tile é imutável — uma vez gerado nunca muda, então cache agressivo é seguro. ### Três detalhes do lado do navegador que não são opcionais Medidos no protótipo v2. Sem eles o zoom funciona e parece quebrado. **Borda de tile em número inteiro, calculada pela mesma conta dos dois lados.** Em escala fracionária, posicionar cada tile por `coluna × tamanho × escala` deixa uma linha de fundo aparecendo entre eles. A borda direita de um tile tem que ser o mesmo inteiro que a borda esquerda do vizinho: ```js const x0 = Math.round(c * ts * f); const x1 = Math.round(Math.min((c+1) * ts, larguraDoNivel) * f); // left = x0, width = x1 - x0 ``` **Camada de fundo permanente.** O nível mais baixo da pirâmide, esticado no tamanho todo, por baixo dos tiles e nunca removido. Custa um tile e elimina o buraco preto enquanto os tiles do nível novo carregam. **Um desenho por quadro.** Arrastar dispara dezenas de eventos de ponteiro por quadro. Redesenhar em todos trava. Agendar em `requestAnimationFrame` e descartar os pedidos repetidos. **Empacotamento opcional.** Uma rodada de 14 imagens gera 1.461 arquivos. Com 600.000 inodes isso dá mais de 400 rodadas, então não é urgente. Se apertar, empacotar os tiles num binário único por imagem com índice JSON, e o endpoint faz `fseek` até o offset — cai para 42 arquivos por rodada. Meia hora de implementação. ## Autenticação **Equipe:** e-mail e senha, `password_hash()` com `PASSWORD_DEFAULT`, sessão em cookie `HttpOnly` `Secure` `SameSite=Lax`. Limite de tentativas por IP e por conta. **Cliente:** token de 32 caracteres aleatórios na URL, coluna `rodadas.token_cliente`. Sem sessão persistente além de um cookie que guarda qual revisor ele declarou ser. Regenerar o token invalida o link anterior imediatamente. ## Isolamento entre estúdios Toda query filtra por `estudio_id`, vindo da sessão — nunca de parâmetro da requisição. Centralizar o acesso a dados numa camada que injeta o filtro, para não depender de cada desenvolvedor lembrar. Um teste automatizado deve tentar ler recurso de outro inquilino e esperar 404. ## E-mail SMTP externo desde o começo. `mail()` do compartilhado cai em spam, e convite que não chega é o tipo de falha que o usuário atribui ao produto. Mensagens: convite ao revisor, lembrete de prazo, aviso de prazo encerrado, aviso à equipe de que a rodada foi liberada. ## Backup O plano tem backup semanal do provedor. Não é suficiente para dado de cliente. Somar um dump diário do MySQL e cópia do `storage/` para fora do servidor. ## Quando sair do compartilhado Sinais: disco perto de 50 GB, processamento acumulando na fila, ou mais de meia dúzia de estúdios pagantes. O destino natural é VPS com a mesma pilha — o código não muda. Storage de imagem pode ir para um bucket com egress grátis antes disso, se o tráfego pesar.