# Fase 2 — a biblioteca de arquivos

22/10/2026, a fatia 27 da fila das telas que faltam (ARQUIVOS-SERVIDOR). O desenho é
`docs/f3-telas-restantes/tela-arquivos.md`, a seção 2 e as fatias F0 e F1 da seção 3. A tela nova é a
fatia 28 (a biblioteca) e o cadastro, a 29; esta fatia é só o servidor: a tela antiga consertada, o
nginx que fecha `/arquivos/` e a API de leitura e de entrega.

A biblioteca é o material da franqueadora — 109 PDFs e planilhas, em duas categorias ("Materiais" e
"Manuais Franquia") — **para a rede inteira**: a tabela `arquivos` não tem unidade. Pela resposta 135 (a)
do dono, ela continua aberta a todos que têm `configuracoes/arquivosExibir/ler` (111 não administradores
em 54 unidades e os 10 administradores), sem recorte por categoria nem por unidade.

---

## O recorte

| endpoint | verbo | origem |
|---|---|---|
| `/adm/api/arquivos?categoria=&subcategoria=&busca=&estado=&pagina=` | `GET` | `ArquivosUsuariosController::indexAction` e `ArquivosController::indexAction` (o estado inativo) |
| `/adm/api/arquivos/categorias` | `GET` | o menu do `layout.phtml` (as categorias ativas) e as contagens que a tela antiga não tinha |
| `/adm/api/arquivos/:id/conteudo?disposicao=inline\|anexo` | `GET` | `ArquivosUsuariosController::abrirAction` e `::downloadAction`, pelo `EntregaDoArquivo` |
| `/adm/api/arquivos/:id/acessos?pagina=` | `GET` | nenhuma: quem abriu e quem baixou o arquivo (22/10/2026, a fatia 29b, a resposta 253 (c) do dono), só ao administrador que cadastra; a entrega das duas portas grava a linha (`RegistroDeQuemAbriu`) |

**Fica de fora:**

| o quê | por quê |
|---|---|
| o envio, a troca, o editar e o estado | a fatia 29 (ARQUIVOS-CADASTRO), com a escrita em multipart da regra 8 de `docs/convencao-api.md` |
| os órfãos (1 JPG aqui, talvez 2 na produção) | a pergunta 2 do desenho, e é mexer na produção: só com a ordem do dono |
| o nginx da produção e do BVIP | não está no repositório (o D12); o passo vai escrito no `procedimento-de-corte-etapa-1.md` (o vhost do passo 8) e no `procedimento-de-corte.md` (o item 5 de "Preparação") |
| as perguntas 2 a 6 de arquivos | as fatias 28 e 29 |

---

## As guardas

- **Ler** é `configuracoes/arquivosExibir/ler`, nas três rotas (o construtor define a `funcionalidade`, o D123).
- **O inativo** é só de quem escreve no cadastro (`arquivosCadastrar/escrever`) **e** é administrador — a
  decisão 1 do desenho, o molde dos convênios. `?estado=inativo` de quem não pode é `403`; o conteúdo de um
  inativo, para ele, é `404`.
- **Categoria inativa:** o arquivo dela não aparece na lista nem nas categorias para ninguém, e o conteúdo é
  `404` para quem só lê. A lista antiga da biblioteca e o "abrir" do ERP passaram a dizer o mesmo
  (`RegraDoArquivo::aQuemLe`); a lista antiga do cadastro continua mostrando, para quem administra, e o
  "abrir" e o "baixar" do ERP abrem o inativo e o de categoria inativa para quem escreve no cadastro E é
  administrador, como a API (`RegraDoArquivo::veOInativo` e `abre`; a verificação, o D571 -- antes davam
  404 a ele também).
- **O id** passa pela `RegraDoArquivo::id` nas duas portas e no cadastro antigo: o número que a coluna não
  guarda é `404`, e não o 500 do PostgreSQL (o D572).
- **Não há guarda de unidade:** o arquivo é da rede. No PostgreSQL quem separa é o inquilino (RLS).
- **Nenhum GET grava.**

## O recurso

`ArquivoJson::daListagem`: `id`, `nome` (o `nome_personalizado`), `descricao`, `categoria` (`id` e `nome`),
`subcategoria` (a vazia e a nula são `null`), `extensao` (sem o ponto), `link`, `ativo`, `enviadoEm`,
`alteradoEm` (ISO 8601) e `conteudo` (a URL da entrega). **O nome físico e o `hash` não saem.** A lista é o
envelope de coleção, com 50 por página (`RegraDoArquivo::POR_PAGINA`), na ordem da biblioteca: a categoria,
a subcategoria (a vazia primeiro, nos dois bancos), o nome, o id.

`GET .../categorias`: `{categorias: [{id, nome, arquivos, inativos, secoes: [{nome, arquivos, descricao}]}],
verInativos, limiteDoArquivo}` — a seção traz a descrição quando todos os arquivos dela têm a mesma (os
Materiais) e `null` quando cada um tem a sua (os Manuais), pela `RegraDoArquivo::secoes`; `inativos` é a
contagem só para quem os vê; `limiteDoArquivo` é o menor entre o `upload_max_filesize` e o `post_max_size`,
em bytes, para a recusa antes de enviar da fatia 29.

## A entrega

A regra 10 de `docs/convencao-api.md`. Em resumo: o `200` é o arquivo, com o tipo, o `Content-Disposition`
limpo, o `nosniff` e o `Cache-Control: private, no-store`; as recusas são o JSON de sempre; a sessão é solta
depois das guardas. O `EntregaDoArquivo` é o mesmo do ERP.

## Os filtros

O que não serve é `422` com o campo: `categoria` que não é número, acima do `int` da coluna, inexistente ou
inativa; `estado` fora de `ativo` e `inativo`; `busca` e `subcategoria` que não são texto ou passam da
coluna; `disposicao` fora de `inline` e `anexo`. A página que não é número vale 1. A busca é por palavras do
nome, como a da lista antiga.

## Provado

- `ArquivoApiTest` (os dois bancos): as guardas das três ações, o ativo de categoria ativa, o recurso sem o
  nome físico, a busca, o inativo só a quem escreve e é administrador, os 422, a página, as seções, a entrega
  com o cabeçalho, o 404 do que não se vê, do nome fora da forma e do que sumiu do disco, e as duas portas
  entregando o mesmo; os dois repositórios, direto.
- `TelasDosArquivosDoErpTest` e `ArquivosTravessiaTest` (os dois bancos): a F0, abaixo.
- `ensaiar-arquivos-adm-api.php` (o nginx de pé, e no CI o `php -S`): `/arquivos/<nome>` de arquivo que
  existe no disco é `404` sem sessão; a entrega com sessão é `200` com os bytes do disco; o ERP entrega os
  mesmos; o menu antigo traz as categorias. `ensaiar-permissao-adm-api.php` (as três rotas, `403` sem o
  privilégio) e `ensaiar-recusas-adm-api.php` (os casos 179 a 187).
- **A verificação (22/10/2026)** acrescentou: o `/arquivos/` sem caixa e com o caminho torto
  (`/ARQUIVOS/`, `//arquivos/`, `/./arquivos/`, `%61`, `/x/../`) no ensaio; o `403` do `?estado=inativo`
  para quem só lê e o `99999999999` nas duas portas, pelo HTTP; e os casos do D567 no link e no lote, da
  edição sem arquivo novo, da ordem do link, da categoria que se cria, do administrador no "abrir" do ERP e
  do id fora do `int4`.

## A F0, na tela antiga

- **O D355:** o nginx fecha `/arquivos/` sem caixa (o "Bloco de segurança 0" de
  `docker/nginx/default.conf`, `location ~* ^/arquivos/`, a primeira regex -- o `^~` deixava passar
  `/ARQUIVOS/<nome>`), o `public/index.php` fecha o mesmo diretório no `php -S` do CI, pelo `realpath`, e o link que substitui um arquivo o tira do
  disco. O `conferir-protecao-do-webroot.php` ganhou a pergunta 4.
- **O D567:** o "Inativo" do formulário desativa.
- **O D568:** o lote é tudo ou nada, diz qual arquivo falhou, e a troca só apaga o antigo depois de gravar.
- **O D361, a linha dos Arquivos:** os filtros das duas listas conferidos, e o link da paginação codificado.
- **O D569:** as categorias do menu são lidas só quando o layout é desenhado, numa consulta, sem as
  subcategorias e sem a cópia na sessão — nenhuma consulta num pedido de `/api` ou `/adm/api`.
- **O D570:** os tetos dos campos são os da coluna (255 no nome e na descrição, 100 na subcategoria).
- **Menor:** o construtor de `ArquivoCategoria` chamava um `setHash()` que não existe (provado pela
  verificação: `ArquivosTravessiaTest::testACategoriaSeCriaEGrava`).
- **Da verificação:** o D571 (o "abrir" do ERP para o administrador) e o D572 (o id e a categoria crus).

---

## A F3, a escrita (22/10/2026, a fatia 29, ARQUIVOS-CADASTRO)

A tela é o modo cadastro da biblioteca ("Configurações › Cadastro de arquivos"); o registro dela está na seção 6 de
`docs/f3-telas-restantes/tela-arquivos.md`. Com as respostas 249 a 252 (a) do dono; a 253 (c), registrar quem abre, é a fatia 29b.
Depois da verificação, o `PUT` aceita também o `alteradoEm` da leitura (409 `mudou` quando o arquivo mudou no meio; o D580), e
`/categorias` manda, a quem cadastra, as `subcategorias` dos ativos e dos desativados.

| endpoint | verbo | origem |
|---|---|---|
| `/adm/api/arquivos` (multipart: `nome`, `descricao`, `categoria`, `subcategoria`, `arquivo`) | `POST` | `ArquivosController::salvarAction` (o novo), UM arquivo por pedido |
| `/adm/api/arquivos/:id` (JSON: os mesmos quatro campos de texto, só os que vêm) | `PUT` | o mesmo `salvarAction` (o editar), sem o arquivo |
| `/adm/api/arquivos/:id/arquivo` (multipart: `arquivo`) | `POST` | o editar com um arquivo novo |
| `/adm/api/arquivos/:id/desativacao` e `/ativacao` | `POST` | `ArquivosController::ativarRemoverAction` |

- **As guardas:** a sessão (401); `configuracoes/arquivosCadastrar/escrever`, pelo `getAcessoDe` (403); ser administrador
  (403, `RegraDoArquivo::FRASE_DO_ADMINISTRADOR`); o id pela regra (404); o corpo acima do `post_max_size` (422 no `arquivo`);
  e o corpo, campo a campo (`RegraDoArquivo::conferir`): a chave que não se altera por ali é 422 com o nome dela -- o estado, o
  `link` (a 252), o `arquivo` como texto.
- **O arquivo** (`RegraDoArquivo::conferirEnvio`): um por pedido (`arquivo[]` é 422), o `error` do PHP, a extensão, o tamanho e o
  conteúdo (`%PDF-` no PDF, `PK\x03\x04` na planilha). O nome físico é `bin2hex(random_bytes(20))` mais a extensão, e o
  `nome_original` guarda o nome enviado, cortado em 255.
- **O serviço** (`ArquivoService::cadastrar`, `::alterar`, `::trocarConteudo`, `::definirEstado`): guarda, grava, e só então
  apaga o antigo; a linha que não grava apaga o guardado e o erro sobe; o estado com a linha travada (409). A subcategoria
  canônica (`::subcategoriaCanonica`) vale também no `salvar` da tela antiga (o D576).
- **O recurso** de cada escrita é o da lista (`ArquivoJson::completo`), sem o nome físico.

**Provado:** `ArquivosPelaApiTest` (19 casos, os dois bancos), `RegraDoArquivoTest` (os casos novos, com
`formularioDoArquivo.casos.json` nos dois lados), `ensaiar-arquivos-adm-api.php` (o envio multipart de verdade, 32 casos, que
apaga o que guardou), `ensaiar-permissao-adm-api.php` (as cinco rotas, 403 sem o privilégio) e `ensaiar-recusas-adm-api.php`
(os casos 188 a 191).
