# Procedimento de corte, etapa 1 — o código novo sobre o MySQL de produção

Escrito em 14/10/2026, no dia em que o dono partiu o corte em duas etapas:

> *"escreva a variante do procedimento de corte. guarde todo o trabalho feito do lado do postgres, mas vamos colocar a
> migração do mysql para o postgres em um segundo momento."*

**Este procedimento nunca foi executado nem ensaiado.** Ele junta o que já está escrito e provado em outros documentos —
o passo 0 e o crontab do `procedimento-de-corte.md`, o dia 0 do `dia0-remediacao-producao.md`, a montagem do ambiente de
teste — com a parte que nenhum deles tinha: pôr o código novo no servidor. Nenhum passo do procedimento põe o código no
servidor, nem a tela nova em `/novo/` (o `dist`, as `location` do nginx): o deploy só aparece por implicação ("o codigo
novo esta no ar", no passo 9; "o mesmo deploy", no passo 3), e a tela nova, só como referência de comportamento. Onde não
sei, está escrito **a medir na produção**.

**As quinze escolhas do dono (CORTE-EM-DUAS-ETAPAS, as perguntas 189 a 203) foram respondidas na página do lote em
14/10/2026, e estão aplicadas aqui** — cada passo, conferência e volta segue o que ele escolheu, junto com as respostas
anteriores que valem para o corte (a 5, a 7, a 10, a 11, a 31 e a 128, a 50, a 62, a 83, a 106, a 122, a 126, a 127, a
158, a 182 e a 184 a 186). A tabela de todas, com onde cada uma entrou, está no fim ("As decisões do dono"). **As seis
que voltaram ao lote** (CORTE-ETAPA-1-ESCOLHAS) **foram respondidas em 15/10/2026, todas na opção (a), e estão aplicadas
aqui** (as perguntas 204 a 209): o backup da janela é o dump da rotina, disparado à mão (a 204); a memória do MySQL e os
três do php-fpm entram na janela (a 205); as entregas depois dela saem à noite, como a janela (a 206); depois de um recuo do
crontab, o novo entra na noite seguinte (a 207); as duas guardas das dez rotas que não leem a sessão ficam (a 208); e o
retrato dos royalties pulado se refaz à mão no mesmo dia (a 209). **E a da verificação das dez rotas**, a 226, na opção (b):
o V2 e o V3 recusam já o colaborador da unidade inativa. **E as duas da verificação do mesmo dia, respondidas também em
15/10/2026, na opção (a):** o teto curto do pool do site só depois de o dono declarar o fim da volta, e até lá os dois
pools com o teto das rotinas (a 228, o **D497**; o passo 8 e "Depois da janela"); e a listagem das travas do servidor
antes do corte, com a decisão do dono sobre a trava de marketing de outro usuário antes de o código subir (a 229, o
**D499**; a leitura 22 e o item 12 de "pronto"). **A janela não começa sem o backup** — e o dele ainda pede quatro
fatos do dono (o passo 3) —, nem sem o que falta escrever e ensaiar ("O que falta fazer antes da janela").

| etapa | o que muda na produção | onde está |
|---|---|---|
| **1** | o código novo — o ERP com as correções, a tela nova em `/novo/` e a API em `/adm/api` — **substitui** o antigo, sobre o MySQL de produção, com o passo 0 aplicado | este documento |
| **2**, num segundo momento | a migração do MySQL para o PostgreSQL | `procedimento-de-corte.md`, guardado inteiro, e o ensaio dele, `ensaio-geral-do-corte.md` |

---

## Por que em duas etapas

A janela de antes levava duas mudanças de natureza diferente, e um defeito no meio dela não diria de qual das duas veio:

- **o código novo nunca rodou na produção.** O ambiente de teste (`ambiente-teste.md`) o roda sobre o MySQL, com uma
  cópia anonimizada da produção, e funciona — mas lá não roda cron, os provedores apontam para o loopback e o aplicativo
  dos clientes não aponta para ele ("O que o ambiente não pode fazer" e "O que fica de fora", naquele documento). A etapa
  1 é a primeira vez que o código novo fala com o Asaas, o OneSignal, o WhatsApp, o SMTP e o Yungas de verdade, e que as
  rotinas do crontab rodam nele;
- **a conversão do banco** — o fuso, a carga, o RLS, os papéis — é o que o procedimento de antes cobre, com as seis
  provas da carga e as seis dos papéis.

Separadas, a etapa 1 tem uma volta que **não perde venda nenhuma** — o banco é o mesmo antes e depois —, e a etapa 2
parte de um código já provado na produção, com o defeito de provedor fora do caminho. **O custo fica na etapa 2:** ela
encontra uma produção que gravou por semanas o que só o código novo grava (ver "O que a etapa 2 herda").

---

## Um sistema só gravando: o código novo SUBSTITUI o antigo

Na janela, o código antigo sai e o novo entra no lugar dele — no mesmo endereço, no mesmo diretório, com **um** crontab.
Não lado a lado, porque duas pontas sobre o mesmo banco fazem coisas em dobro e com regras diferentes:

- **as rotinas em dobro.** Dois crontabs disparam os royalties, as faturas do Asaas, os envios de marketing e o NPS duas
  vezes. A casa já pagou por isso: 87 linhas de royalties duplicadas (**D71**) e a fatura emitida de novo quando a
  resposta do Asaas se perde (**D484**). A trava contra execução simultânea é de arquivo, por máquina (**D44**), e no
  código antigo só os envios de marketing e o push a pegam (o `acquireLock` do `CronJobService` da importação,
  `eaf1fca`): as rotinas de royalties rodam sem ela. E a fila `push_app` é lida pelas duas pontas — o WhatsApp e o push
  sairiam em dobro;
- **as regras diferentes.** O código antigo não grava a vigência do horário, o cupom do dia, o pacote recusado, o motivo e
  o autor em Contas a Pagar, nem a vigência da taxa de royalty — o retrato das 05:25 sairia diferente conforme quem o
  rodou; aceita os dois ativos no mesmo login que o novo recusa (**D349**); e tem viva a `/autenticacao/migrar-senhas`
  (**D2**), que sobrescreve com um GET o hash de todos os usuários ativos — e, depois de a etapa 1 rodar, devolveria a
  senha de antes a quem trocou a sua no código novo: a rota reescreve a senha a partir da `senha_php83`, que o código novo
  não grava mais (**D79**; a coluna segue no MySQL), e o login dos dois códigos compara com a senha reescrita. É o que o
  passo 6 desfaz, anulando a coluna (a sua 202 (b)).

**O plano não abre as duas pontas:** a release só chega ao servidor no começo da janela (a sua 201 (a)), e não há endereço
provisório. **Se um dia elas forem inevitáveis por algumas horas** — para abrir o código novo contra o banco de produção
antes de lhe dar o endereço, por exemplo —, a ordem que evita rotina em dobro é esta:

1. o crontab do código antigo continua sendo o **único**: nenhuma linha chama o código novo, e o endereço provisório dele
   nega no nginx as rotas das rotinas, como o teste faz (`/api/cronjobs`, `/api/marketing` e a de cupons, em
   `docker/nginx/teste-host.conf.modelo`);
2. ninguém grava pelo endereço provisório: ele só lê;
3. na troca, o `crontab -r` vem **antes** de o código novo assumir o endereço (o passo 1 da janela, abaixo), e o crontab
   novo só **depois** das conferências (o passo 12).

Mesmo assim, o que o código novo gravar nesse meio-tempo vai para o banco de produção. A troca se ensaia no ambiente de
teste (a sua 199 (a); "O ensaio da etapa 1", abaixo), e não na produção.

---

## O que esta etapa NÃO faz, e por quê

**Nada do PostgreSQL.** Fica para a etapa 2, como está escrito no `procedimento-de-corte.md`: provisionar o PostgreSQL e
acertar o fuso dele; gerar o schema e o roteiro de carga; os papéis; a carga e as seis provas; as seis provas dos papéis;
o `local.php` do PostgreSQL (o passo 7: desde 08/11/2026, o **D329**, ele troca só o bloco `doctrine` do `local.php` guardado, pelo `scripts/trocar-doctrine-do-local-php.php`); a vigência medida no PostgreSQL (o 7b); a passada das tags e a
chave `tags_sem_acento` (o 7c — no MySQL a busca já ignora o acento, e a passada não mudaria nada), com a
`campanhas_sem_acento` das campanhas na mesma linha (o **D618**; sem passada, e no MySQL ela só passa a achar o texto
gravado com a marca do acento solta); o `pdo_pgsql`; o
`bvip_app`; e o que vem depois da carga (o MySQL somente leitura por semanas, o tipo do dinheiro, **D49**). **Por quê:** é
a decisão do dono, e é também o que mantém a volta desta etapa barata — com o mesmo banco, voltar é trocar o código.

**Nada do passo 3 que só o PostgreSQL pede.** O passo 3 do procedimento escreve no MySQL antes do dump, e quase tudo ali
existe porque o PostgreSQL recusaria o dado:

| roteiro | por que não entra na etapa 1 |
|---|---|
| `scripts/pre-migracao-limpeza.sql` | o ano 0 e as duplicatas que os índices únicos do PostgreSQL recusam. O MySQL convive com elas, como hoje: o código novo lê `royalties` com `setMaxResults(1)` e `dashboard_colaboradores` pela "última gravada", e a tela de permissões mostra duas vezes o privilégio duplicado (o `PrivilegioRepository` agrupa por `pri.id`), o que é cosmético. Pela leitura do código; não medido na produção |
| os `UPDATE` de `scripts/medir-logins-com-maiuscula.sql` | é a caixa do PostgreSQL; no MySQL a comparação já ignora a caixa. **A listagem 4c, não:** ela vem antes da etapa 1 (a leitura 16, abaixo) |
| `scripts/limpar-dicionario-cep.sql` | irreversível, e o código antigo não tem o ViaCEP: rodado aqui, uma volta ao código antigo perde o preenchimento por CEP de vez. O código novo funciona com o dicionário cheio (**D74**) |
| `scripts/remover-funcionalidade-vendas-caixa.sql` | com ele aplicado, o código antigo não deixa ninguém entrar: o `SessaoAcl` de antes lê "Vendas caixa" em todo login (**D227**). O código novo funciona com ou sem a linha |
| os `UPDATE` de `scripts/medir-nascimento-com-hora.sql` | dependem do bloco 4 da limpeza: com uma data anterior ao ano 1000, o modo estrito recusa o `UPDATE` de `clientes` inteiro |
| `scripts/migracao-privilegios-unicos.sql` | apaga linha — as gêmeas de `privilegios_usuarios`, juntadas na mais antiga com o OU do par, e o molde repetido de `privilegios`, ficando o mais antigo (a sua 237 (c), 17/10/2026) — e só depois cria a chave única no molde e nas pessoas (a sua 125 (a), 17/10/2026; **D289**, **D374**). O código novo funciona com as gêmeas e sem as chaves, como hoje: soma as linhas ao ler e deixa o par numa linha ao gravar. Com a chave das pessoas, o "novo" da janela de permissões do código antigo, num par que já tem linha, dá erro numa volta, em vez de gravar a gêmea |

**Decidido: a sua 192 (a), "Só o Líder entra na etapa 1"** (o passo 6 da janela, com a linha que o desfaz); o resto fica
na etapa 2, como está escrito — e com ele a sua 122 (a), o "Vendas caixa" apagado no passo 3, e a sua 10 (b), o dicionário
de CEP esvaziado no passo 3. Os três privilégios que ficaram sem tela (a sua 182 (a)) ficam na lista, sem efeito: nada a
apagar em etapa nenhuma. **E, de qualquer jeito, antes da janela:** os pares de dois ativos no mesmo login ou e-mail (a
listagem 4c, a leitura 16), um a um, pelo dono — o código novo já os recusa no MySQL.

**Nunca, nesta etapa nem na outra:** o `orm:schema-tool:update --force` contra o banco de produção — com o código novo,
ele apagaria as colunas que as entidades não mapeiam (`scripts/conferir-schema-tool.sh`); o `sql/sessoes.mysql.sql` (a
sessão fica em arquivo, **D198**); a suíte de caracterização ou a de contrato contra a produção, e as fixtures nela (o
passo 8 do procedimento, **D378**); e chamar à mão, para testar, `relatoriosRoyaltiesFaturasGerar` ou
`relatoriosRoyaltiesFaturasAtualizar`: elas cobram franqueados de verdade.

**A `unidades.iframe_bi` fica** (**D9**), como no procedimento: tirá-la é decisão à parte.

---

## Antes da janela

### O que se lê na produção — só leitura

Nada disto muda o servidor, e nada disto foi lido daqui: tudo é **a medir na produção**. São as lacunas que
`operacao-atual.md` marca [A CONFIRMAR] e as que nenhum documento chegou a anotar, e cada linha decide alguma coisa da
janela. Leia os arquivos de configuração, e nunca uma página `phpinfo`; das credenciais, só os nomes dos arquivos onde
estão, nunca os valores — e o crontab de lá traz o token das rotinas na URL: leia-o com o token tirado, como abaixo.

**Nada do repositório vai para o servidor.** A árvore de lá é a da importação (`eaf1fca`), que não tem `scripts/`: um
roteiro daqui roda pela entrada do `ssh`, e a saída fica na máquina de quem roda — a forma do 9 de "O que hoje bloqueia"
do procedimento. Nenhum comando abaixo grava arquivo no servidor.

```sql
-- no franquia_producao, o banco de producao (o laco do passo 0 e a docs/ambiente-homologacao.md)
SELECT VERSION(), @@GLOBAL.sql_mode, @@GLOBAL.time_zone, @@system_time_zone,
       @@event_scheduler, @@innodb_buffer_pool_size, @@max_connections;
SELECT EVENT_NAME, DEFINER, STATUS, LAST_EXECUTED FROM information_schema.EVENTS;
```

```bash
ssh root@<a producao> 'cat /etc/os-release; php -v; php -m; php --ini; composer --version; nginx -v; df -h'
# quem agenda: o root e todo outro usuario (o /var/spool/cron/crontabs), o /etc/cron.d e os timers -- o token tirado da URL
ssh root@<a producao> 'grep -rH -D skip . /var/spool/cron/crontabs /etc/cron.d | sed "s/token=[^\"& ]*/token=<tirado>/g"; systemctl list-timers --all'
ssh root@<a producao> 'RAIZ=/var/www/franquiabv.com.br/public_html sh -s' \
    < scripts/comparar-com-producao.sh > impressao-producao.txt                        # so le (D37); a saida fica aqui
```

| | O que | Por que importa na etapa 1 |
|---|---|---|
| 1 | a versão do MySQL | os documentos discordam, e nenhum é uma leitura feita para isto: a coleta de 19/08/2026 deu 8.4.7 sobre Ubuntu 25.04 "no próprio servidor" (`operacao-atual.md`, e a correção de 19/08 em `ambiente-homologacao.md`), e chama o 8.0.46 de "o das réplicas"; o dump de 16/08/2026, "tirado no servidor de produção", em `localhost`, diz 8.0.46 sobre Ubuntu 22.04 (a tabela dos dumps de `operacao-atual.md`); a tabela de papéis de `ambiente-homologacao.md`, o passo 0 e os cabeçalhos de duas migrações dizem "o 8.0.46 de produção". O `ALGORITHM=INSTANT` do "Não" do pacote e das saídas e o `INPLACE` do índice de `id_pai` dependem dela: se o servidor os recusar, o `ALTER` para com erro, sem mudar nada |
| 2 | o `sql_mode` | o repositório roda em modo estrito, sem `ONLY_FULL_GROUP_BY` (o `docker-compose.yml`), e as telas novas foram medidas assim: o texto longo demais vira erro, e não corte calado. O da produção nunca foi lido, só deduzido do que os eventos gravaram no dump |
| 3 | o fuso | o código não chama `date_default_timezone_set`: depende do `date.timezone = America/Sao_Paulo` do `php.ini` (o do repositório é `docker/php/php.ini`), no FPM **e** na linha de comando. Sem ele, toda data do PHP sai em UTC. O do banco quase não pesa: o código não usa mais o relógio dele (**D42**) |
| 4 | o buffer pool | no teste, os 128 MB de fábrica deixaram a comanda e a agenda da tela nova lendo quase tudo do disco, e a pilha passou a 4 GB (**D493**, `docker/mysql/teste.cnf`); o **D189** mediu a consulta da agenda, na cópia local (um laptop, e não o teste), em 21 a 36 s com 128 MB e 0,9 s com 2 GB. O da produção nunca foi lido. **A sua 196 (b):** o índice da agenda entra no passo 0, "além de ler a memória" — e, com a memória da máquina e o que mais roda nela, a ler junto. **E a sua 205 (a):** se der a de fábrica, a memória se ajusta **na janela**, com o sistema parado e antes das migrações (o começo do passo 4), "no valor que a memória da máquina permitir" — o valor sai desta leitura e se escreve antes da janela (o item 7 de "pronto") |
| 5 | os eventos | os dois do **D26** (`limpar_push_app_24h` e `evt_agendas_exclusoes_limpar`), com o `DEFINER` de um usuário que não está em inventário nenhum, e se o agendador está ligado. **A sua 195 (a):** o `limpar_push_app_24h`, a limpeza de hora em hora, é o que se desliga no passo 12 — a leitura confirma o nome e o estado (`ENABLED`) antes, e de novo depois do `ALTER EVENT`; o outro está `SLAVESIDE_DISABLED` desde 19/08/2026 e nunca rodou, e nada se faz nele |
| 6 | o sistema e o PHP | o `composer.lock` **de lá** só aceita o 8.1 (`operacao-atual.md`, "Como a versão do PHP foi determinada"); o do repositório aceita do 8.1 ao 8.3 desde 08/11/2026 (a fatia 130, o **D11**: a suíte inteira verde no 8.2, numa imagem à parte), mas o CI e o contêiner seguem no 8.1, e trocar o PHP do servidor não é desta etapa; o servidor da aplicação é o do banco (`localhost`), e o sistema dele está [A CONFIRMAR] em `operacao-atual.md`: as duas fontes da leitura 1 dão o Ubuntu 25.04 ou o 22.04, e o PHP de fábrica do 22.04 é o 8.1, o do 25.04 não. As extensões que o código pede são as do `docker/php/Dockerfile` — `pdo_mysql`, `mysqli`, `intl`, `zip`, `gd`, `gmp`, `bcmath`, `exif` e `opcache`; o `pdo_pgsql` é da etapa 2 —, mais `mbstring`, `curl` e `openssl` (o push pelo FCM assina com ela, em `module/ISBase/src/Push/ContaDeServico.php`) |
| 7 | o php-fpm | os pools, o `opcache.validate_timestamps`, o `memory_limit`, o `max_execution_time`, o `upload_max_filesize` e o `post_max_size` (os do repositório estão em `docker/php/php.ini`), e se o serviço tem `PrivateTmp` (o `systemctl cat` dele): a trava das rotinas é um `flock` em `sys_get_temp_dir()` (`module/ISBase/src/Util/TravaDeExecucao.php`), e um `php` de linha de comando não a vê atrás de um `/tmp` privado. E o usuário e o grupo do pool (o dono do `local.php` e das pastas graváveis no passo 7; no teste, `www-data`), se o pool tem página de status (`pm.status_path`, que mostra o pedido em curso de cada processo: o passo 2 a usaria), e o `process_control_timeout` (com 0, o de fábrica, o recarregar do passo 9 não espera o pedido em curso). E os três requisitos do **D67**, no item 4 de "Preparação" do procedimento — **a sua 205 (a): na janela**, junto com o vhost novo e o recarregar que ela já faz (os passos 8 e 9), e não "antes da janela", como o procedimento os pôs em 11/10/2026: a sua 201 (a) diz "Nada muda no servidor antes da janela". Esta leitura dá o de onde partem: os pools de hoje e os valores deles |
| 8 | o Composer e o `vendor/` | **o `composer.lock` mudou em 08/11/2026** (a fatia 130, o **D11**: `laminas/laminas-cache` 3.6.0 → 3.13.0, `laminas-cache-storage-adapter-filesystem` 2.3.0 → 2.5.0, e saiu o `stella-maris/clock`): o `vendor/` de lá **não serve** ao código novo, e **o novo sobe na janela**, no pacote do `montar` (o passo 2 da tabela de "A release, montada fora da produção"). A leitura confere só se o `vendor/` da produção saiu do lock antigo (o sha256 do `composer.lock` e do `vendor/composer/installed.json` de lá contra os do commit `281c9c0a`, o último com o lock antigo) — o que diz que a volta ao código antigo volta também ao `vendor/` dele, que fica na árvore antiga |
| 9 | o código de lá contra o repositório | **"Nenhum deploy pode acontecer antes de saber se produção divergiu"** (**D37**): a produção não tem git, e uma mudança feita direto no servidor some na troca, calada. O `scripts/comparar-com-producao.sh` gera a impressão digital de lá, pela entrada do `ssh` (o bloco acima, com o `RAIZ`); a comparação é feita aqui |
| 10 | onde moram as credenciais | só os nomes: o `local.php` da etapa 1 nasce delas (as chaves do passo 3 do dia 0), e as do banco podem ser as que estavam **comentadas** (`franquiabv_user`), e não as ativas |
| 11 | quem agenda | as 25 entradas do crontab do root (a produção está congelada: as quatro rotinas que saíram do código, duas em 13/10/2026, a das 03:10 em 25/10/2026 e a da Evasão das 04:10 em 04/11/2026, seguem nele, chamando o código de lá, e é o primeiro bloco do passo 9 do procedimento que as tira); quem mais chama `/api/cronjobs/` — outro usuário, o `/etc/cron.d`, um timer (o bloco acima) —, porque o passo 1 da janela tem de parar todos, e o passo 12 e a volta, trazê-los de volta; o que apara a `agendas_exclusoes` em 60 dias, que não está mapeado (**D26**); e **o dump de hora em hora** — o `dumpv3.sh`, que o **D33** já nomeia (e o `sistemacompletofbv.sh`, o dos arquivos, diário) —: em que minuto, por qual usuário, por qual agendador e com que comando ele roda, porque ele não pode disparar durante o passo 4, e porque o passo 3 o dispara à mão (a sua 204 (a); o passo 3, abaixo) |
| 12 | o nginx | o `server {}` do site — o `server_name` (o `www` e o raiz), o `root`, o `fastcgi_pass`, as regras de proteção, o `client_max_body_size`, os logs por vhost (**D36**) — não está no repositório (`operacao-atual.md`, "Configuração do nginx"), e a tela nova pede três `location` nele. E por onde o próprio servidor chega a `https://franquiabv.com.br` (o `/etc/hosts`, ou o IP público dele): as sondas dos passos 10 e 12 e todo o crontab saem dele, e a página de manutenção tem de deixá-lo passar. **E os endereços da equipe** que testa (a sua 190 (a): a manutenção deixa passar só a equipe e o próprio servidor) — a lista é do dono |
| 13 | o disco, `data/` e as pastas de upload | a árvore nova e a antiga guardada ficam no mesmo disco enquanto a volta for possível — e a volta, pela sua 198 (a), dura até o dono declarar o fim, no mínimo 48 horas; com as entregas da sua 200 (c), as liberações seguintes também —, e o dump da janela, enquanto ele estiver no servidor (pela sua 204 (a), ele sai para fora do servidor e da rotação; no disco, até a cópia de fora estar conferida). **E o espaço livre no disco do diretório de dados do MySQL para os três `ALTER` que copiam** (o passo 4): cada um monta uma cópia inteira da tabela antes de soltar a antiga — o tamanho de `vendas`, `pontuacoes_historicos` e `cupons_clientes` (`data_length` e `index_length` em `information_schema.TABLES`, só leitura), somado ao dump, se ele ficar no mesmo disco. Com o disco cheio no meio do `ALTER` de `vendas`, o `ALTER` falha e o servidor para de gravar, com as réplicas ligadas |
| 14 | o backup que já existe | **descrito pelo dono na 197** (14/10/2026): de hora em hora um dump local, com 24 horas de rotação; três outras VPS com a replicação do MySQL em tempo real; e o backup diário da Hostinger. O **D33** já nomeia o dump: o `dumpv3.sh`, de hora em hora (e o `sistemacompletofbv.sh`, o dos arquivos, diário). **A forma do backup da janela está decidida: a sua 204 (a)**, o dump da rotina, disparado à mão (o passo 3, abaixo). **O que não se sabe continua a ler, e é do dono** — os quatro fatos que a pergunta 204 pediu e que não vieram com a resposta: se uma restauração de algum deles já foi testada; em que minuto, por qual usuário e com que comando o `dumpv3.sh` roda; se ele leva os eventos e a posição do log binário; e onde ele fica. E o que o diário da Hostinger guarda e por quanto tempo. Sem os quatro, o passo 3 não se escreve com o comando de lá. O `operacao-atual.md`, "Backups", segue [A CONFIRMAR] |
| 15 | `public/app.json` | as telas de administração o reescrevem na produção, e o aplicativo o lê a cada abertura (**D36**): o do repositório é o de julho, e não vai por cima |

E as medidas de dado e de log, também só de leitura:

| | O que | Como | Por que importa na etapa 1 |
|---|---|---|---|
| 16 | dois ativos no mesmo login ou e-mail | a listagem 4c de `scripts/medir-logins-com-maiuscula.sql`, na réplica | o código novo os recusa **já no MySQL** (o 6 de "O que hoje bloqueia" do procedimento, feito em 04/10/2026): quem divide o login ou o e-mail com outro ativo não entra no primeiro dia. Cada par é do dono, um a um, **antes** da janela |
| 17 | as senhas | `scripts/medir-senhas-producao.sql` (só contagens, **D78**) | a senha que autoriza no balcão passou a `password_verify` contra o bcrypt, e não mais o `getSalt()`: quem não tem um bcrypt válido deixa de autorizar |
| 18 | quem chama as três `unidadeVendas*` | o 9 de "O que hoje bloqueia" do procedimento | elas seguem no código novo, e só o log diz quem as chama |
| 19 | as versões do aplicativo | `ssh root@<a producao> 'php -- --dir=/var/log/nginx' < scripts/inventariar-versoes-app.php > versoes-do-aplicativo.txt` (a W3; por esse caminho a seção das rotas sem chamada sai vazia, porque ela lê o `module/` daqui, e a contagem das versões não muda), e quantos pedidos por dia recebem as dez rotas da seção abaixo | com as dez abertas como hoje (a sua 189 (b)), é o número que diz quantos clientes o fechá-las vai atingir, quando o aplicativo novo estiver em campo |
| 20 | o log do sistema | o teste do 8 de "O que hoje bloqueia" do procedimento — o `/usr/bin/logger` existe, escreve uma linha com a marca `rotinas` e uma marca de agora, e a contagem dela no journal (`journalctl -t rotinas`) e no `/var/log/syslog*` (pelo `zgrep`); escreve uma linha no log e não muda nada —, e por quanto tempo o log do sistema guarda o que recebe | **a sua 128 (b): sem e-mail, o aviso de erro das rotinas vai ao log do sistema** — "alguém tem de olhar o log". Sem programa de e-mail, o cron não manda a saída a lugar nenhum: o aviso é escrito pela própria linha do crontab, com o `\|\| logger -t rotinas` do passo 9 (escrito e ensaiado em 07/11/2026, o item 4 de "O que falta"). Com a contagem em zero, ou sem o `logger`, o aviso não chegaria a lugar nenhum, e volta a você antes da janela |
| 21 | as três réplicas | `SELECT VERSION()` e `SHOW REPLICA STATUS` em cada uma, só leitura | a versão de cada uma contra a da produção — o parque é misto, e os documentos discordam de qual é qual (a leitura 1, o **D27** e `operacao-atual.md`); replicar de uma versão maior para uma menor não é suportado —, qual delas é a contingência de hoje, a que se abre só para leitura da agenda (`alta-disponibilidade.md`), e se os dois fios estão rodando, sem erro e sem atraso. Cada réplica reexecuta os `ALTER` do passo 4 depois de o primário terminar, um de cada vez, e a que recusar uma DDL para de replicar calada: é a conferência dos passos 4 e 13 (abaixo) |
| 22 | as travas das rotinas | `ssh root@<a producao> 'ls -la /tmp/*.lock; sysctl fs.protected_regular'` — o dono, o grupo, o modo, a data e o tamanho de cada arquivo, e a proteção do kernel para o arquivo de outro dono numa pasta como o `/tmp`, tudo só leitura —, e a mesma listagem no `/tmp` privado do php-fpm, se a leitura 7 der `PrivateTmp` (o caminho, a medir, sai do `systemctl cat` dele) | **a sua 229 (a), "Conferir antes do corte"**: o código novo abre a trava de outro dono só para ler e trava igual (o **D499**), e uma rotina que hoje esteja parada no servidor por uma trava de outro usuário — alguém que a rodou à mão pelo terminal, como administrador — volta a rodar quando o código subir, sem ninguém ter pedido. No código de lá, só os envios de marketing usam a trava (o `CronJobService::acquireLock` da importação): `marketingEnvios.lock`, `marketingPushgeral.lock`, `marketingPushnotificarAniversario.lock` e `marketingPushclientesNotificarSentimosSuaFalta.lock`. **Uma delas com dono diferente do usuário do pool (a leitura 7) se lê pelo modo**, porque os dois códigos abrem o arquivo de jeitos diferentes: o de lá só para escrever (`fopen(..., 'c')`), e o novo para escrever e, se não puder, para ler (o `TravaDeExecucao::obter`). **(a) O pool pode escrever** (o `w` nos outros, como `-rw-rw-rw-`, ou no grupo dele): o envio roda hoje e segue rodando, e não há o que decidir — fora a proteção do kernel: com `fs.protected_regular` diferente de 0, a trava de um dono que não é o root nem o pool não abre para escrever nem assim, e o caso é o (b). **(b) O pool só pode ler** (como `-rw-r--r--`, a de quem a criou como root, com a máscara de fábrica): o envio está parado hoje e **volta a rodar quando o código subir**. **(c) O pool nem lê** (como `-rw-------`, a de uma máscara 077): parado hoje, e **segue parado com o código novo**, agora com a exceção do `TravaDeExecucao::obter` a cada disparo; voltar a rodar pede apagar a trava no servidor, uma escrita. No (b) e no (c), **o dono decide, antes de o código subir, se aquele envio volta a rodar ou fica parado** (o item 12 de "pronto"). As outras rotinas só ganham a trava com o código novo, e nenhum arquivo delas existe lá |

### As dez rotas do aplicativo instalado: abertas como hoje (a sua 189 (b))

De 03/09 a 14/10/2026 o código novo **trancava dez rotas do aplicativo dos clientes**: sem uma sessão do ERP, elas
respondiam 401 (o **D59**, as de ler, e o **D91**, as de escrever; em `module/ISApi/src/Controller/AplicativoController.php`,
e nos snapshots `*-sem-autenticacao.json` da suíte de contrato): `sorteiosSelecionarCupons`, `cuponsSelecionar`,
`clienteSelecionar`, `indicacoesListar`, `indicacaoCadastrar`, `getClienteAgenda`, `clienteConsumoSelecionar`,
`agendaSalvarV2`, `agendaSalvarV3` e `agendaDesmarcar`. O aplicativo instalado não tem sessão nenhuma: **trancadas, no dia
da troca ele deixaria de marcar e de desmarcar horário, e de mostrar a agenda, os cupons, os sorteios, o consumo e as
indicações.** O D91 escreveu o custo em 03/09/2026 — "o aplicativo instalado deixa de marcar e de desmarcar, que é a função
principal dele" — e amarrou o deploy ao aplicativo novo em campo (o passo 2 do D59), que fala o `/api/v4`. **O aplicativo
novo não existe** (`push-guia-do-aplicativo.md`: "Falta o aplicativo novo"). Na cópia, 71.402 atendimentos de 2026 vieram
pelo aplicativo (a primeira linha do bloco, `id_pai` nulo e `origem = 'app'`, pela data do horário; contado em 14/10/2026 e
de novo em 15/10/2026 — o comentário de 12/10 dizia 71.404, numa conta que não ficou escrita).

Nem a suíte de contrato nem o ambiente de teste falam com o aplicativo de verdade. De 03/09 a 14/10/2026 os snapshots das
dez gravaram o 401 de propósito, e a frase de `api-aplicativo.md` — "Estes endpoints não podem mudar durante a migração" —
não valia para elas.

**Decidido: a sua 189 (b)** — *"A etapa 1 sobe com as dez rotas abertas ao aplicativo de hoje: como estão hoje na
produção, e elas fecham quando o aplicativo novo estiver em campo. A exposição fica a de hoje, nem maior nem menor."*

**Feito em 14/10/2026** (AS-DEZ-ROTAS-ABERTAS; o commit desta entrega), e a release o leva: **as dez respondem sem
sessão**, com a mesma entrada e a mesma resposta da produção de hoje — o corpo, o status e o `Access-Control-Allow-Origin:
*` que nove delas mandavam no código de julho (a V3 nunca o mandou; o D59 o tinha tirado com o login, e o aplicativo é
chamado de outra origem, o `capacitor://`). O interruptor é **uma constante**, `AplicativoController::ROTAS_DO_APLICATIVO_INSTALADO`,
que sai `abertas` — no código, e nunca no `local.php` —, com a guarda `usuarioDoSistemaAutenticado` e o recorte da 106
escritos atrás dela. **Fechá-las depois é trocar a constante para `fechadas`**, numa entrega, quando o aplicativo novo
estiver em campo: o 401 e o recorte voltam juntos, o cabeçalho sai, e foi medido — com a constante trocada, a suíte
de contrato passa 74 de 74 com os snapshots de fechadas, e o ensaio das escritas passa entrando como o gerente. Os
snapshots dos dois estados ficam: abertas, os de antes do D59 (`cupons-selecionar`, `agenda-desmarcar-sem-dados`…),
repostos do git byte a byte e verdes sem regravar; fechadas, os nove `*-sem-autenticacao.json` das rotas antigas, mais
o `cliente-selecionar-sem-autenticacao`, o `cliente-selecionar-post-sem-autenticacao` e o
`cliente-consumo-post-sem-autenticacao` (os 401 que moravam nos nomes antigos). O
`scripts/ensaiar-escritas-do-aplicativo.php` prova as escritas **sem a sessão do ERP**, de outra origem, como o
aplicativo instalado, nos dois bancos de contrato; e `api-aplicativo.md` volta a dizer que estes endpoints não mudam, até
o aplicativo novo estar em campo. O caso 20 do `scripts/ensaiar-sessao-do-erp.php`, que roda no CI, segue o interruptor
(abertas, o aplicativo sem o cookie chega à ação; fechadas, o 401): até a verificação de 15/10/2026 ele exigia o 401
sempre, e saía 1. A tabela da guarda do login (`module/ISConfiguracao/config/rotas-publicas.php`) diz o que defende as
dez nos dois estados — abertas, nada além do teto de taxa da busca pelo telefone.

**O que ficou diferente do código de julho nessas dez**, e não é de quem chama (lido ação a ação, com o serviço e as
consultas que elas chamam, contra o `eaf1fca`, na verificação de 15/10/2026): o `status` do sorteio, texto e não nulo (o
D56); os cupons — o do produto desativado não aparece (o D274, a sua 96 (a)) e o desconto e o produto são os que o cupom
guardou (o D271, a sua 94 (a), com as colunas do passo 0); o serviço que não existe, recusado com 200 onde julho dava 500;
a ordem do que empata, pelo id; as duas guardas abaixo; e, desde 15/10/2026, o colaborador ativo de uma unidade inativa,
que o V2 e o V3 recusam (a sua 226 (b), abaixo). **E o que ficou IGUAL a julho, que a primeira versão desta seção dava
como recusado:** a data que não existe vira outra (31/02 grava em 03/03; o D420 e o D432 são do `dataValida`, que o marcar
não chama); e o V3 grava o corpo do pedido em `data/logs/agenda_json/` antes de qualquer conferência, agora sem sessão. A
lista inteira, com os números, está no **D91**.

**A sua 106 (a) conflitava com reabrir, e saiu junto enquanto as dez estiverem abertas.** O recorte da 106 — "pela
unidade da sessão, como a agenda do ERP" — está no `colaboradorDaUnidadeDaSessao`, que lê a unidade **da sessão**: sem
sessão, ela é nula, e o recorte devolveria "não é da minha" para todo colaborador — o `agendaSalvarV2` e o `V3` diriam
"Informe o colaborador.", e o `agendaDesmarcar`, "ID inválido.". O código já previa isso (o comentário da guarda, o
**D237**), e é o que a 189 (b) pede: "como estão hoje na produção", e hoje a produção não recorta. Com as dez abertas o
recorte não é perguntado; a 106 continua decidida para quando elas fecharem: com a sessão de volta, ele volta com ela.

**As duas guardas que não leem a sessão ficam — decidido: a sua 208 (a)**, *"As duas ficam: o que o aplicativo faz não
muda; se a contagem na cópia der mais que zero, o número volta a você antes da janela. A exposição fica a de hoje, menos
essas duas portas."* São o serviço da unidade do colaborador (`servicoDaUnidadeDoColaborador`, a verificação da 106) e o
horário de bloqueio sem cliente, que não se desmarca (a resposta 158, *"o aplicativo não muda"*). Nenhuma das duas existe
na produção de hoje, e nenhuma lê a sessão: ficam nos dois estados do interruptor, como o código já fazia desde 14/10/2026
— nada mudou no código por ela. **A contagem deu zero, e nada volta ao dono** (o item 15 de "O que falta medir"): **0 dos
71.402 atendimentos de 2026 pelo aplicativo têm o serviço de outra unidade que não a do colaborador** — 341 de 969.426 em
todos os anos, o último em 2025. Nenhum atendimento que o aplicativo gravou em 2026 teria sido recusado pela guarda do
serviço.

**E o colaborador da unidade inativa no V2 e no V3 — decidido: a sua 226 (b)**, *"As duas recusam já: o colaborador da
unidade fechada vira o que não existe, com a mesma resposta que elas dão hoje ao número que não acha ninguém ('Informe o
colaborador.'), como a marcação nova — a sua 179 (b). Fecha um pouco mais do que a produção, e só para o pedido montado à
mão."* A 179 (b) tinha fechado a agenda dele e a marcação da v4, e deixado estas duas de fora porque elas exigiam a sessão;
a 189 (b) as abriu sem ela, e um pedido montado à mão, sem credencial, marcava na agenda da unidade fechada, como a
produção de hoje. **Feito em 15/10/2026** (RESPOSTAS-DE-15-10): o `AplicativoController::semOColaboradorDaUnidadeInativa`,
a mesma regra da v4 (`Usuario::daUnidadeInativa`), chamado pelo V2 e pelo V3 **nos dois estados do interruptor** — a guarda
não lê a sessão; fechadas, quem tem "Agendas unidades" também marcava nele. Provado em `RotasAntigasDoAplicativoTest`
(`testAbertasOMarcarSemSessaoRecusaOColaboradorDaUnidadeInativa`, o que antes prendia o "marca", e mais dois: a resposta
igual à do id que não existe, e o estado fechado), e vigiado no `scripts/conferir-recorte-por-unidade.php`. Na cópia, 99
colaboradores ativos em 24 unidades inativas, e nenhum atendimento pelo aplicativo com eles criado nos 90 dias antes da
cópia: o aplicativo instalado não sente.

### A release, montada fora da produção

A produção não tem git (**D37**), e o deploy lá é cópia de arquivo. A etapa 1 leva uma árvore inteira, montada numa
máquina que não é a produção, a partir do commit que passou nos portões — e não da árvore de trabalho de ninguém. **O
roteiro que faz isso é o `scripts/publicar-producao.sh`** (08/11/2026, a fatia 129; o **D37**), no molde do deploy do
teste: o `montar` (na sua máquina, com o docker) faz os quatro itens abaixo; no servidor, o `levar` faz a pasta ao lado do
passo 1, o `trocar` faz o passo 7 até a troca das árvores e a pasta da tela nova do 8 (o vhost e o bloqueio do 8 seguem à
mão), o `entregar` é o `trocar` com o recarregar e a conferência (as entregas), e o `voltar` faz o item 3 de "A volta", com
os nomes de lá, e para antes do recarregar do item 4 (o passo 7 e "A volta" dizem onde ele entra). **Escrito e nunca rodado em servidor nenhum**: só
no ensaio local (`scripts/ensaiar-publicar-producao.sh`, num contêiner descartável), e o ensaio no BVIP (a sua 199 (a))
segue por fazer -- o dono revisa antes da janela.

| | O quê | Como | Confere |
|---|---|---|---|
| 1 | o código | um checkout limpo do commit, e dele o `git archive`: a produção recebe arquivos, e não o repositório — o histórico guarda as credenciais antigas (**D3**) | o commit anotado ao lado da release |
| 2 | o `vendor/` | `composer install --no-interaction --prefer-dist --no-progress` numa imagem do `docker/php/Dockerfile` (o PHP 8.1 e o Composer 2.2, que o `laminas-dependency-plugin` 2.7 ainda exige), como na função `subir_e_ajustar` do deploy do teste. **Desde 08/11/2026 o lock é outro** (o **D11**): este `vendor/` é o que sobe na janela, e o da produção não serve mais ao código novo (a leitura 8). Com ou sem as dependências de desenvolvimento é a medir: o teste instala com elas | `git status --porcelain --untracked-files=no` vazio: o composer não mudou arquivo versionado (o mesmo freio do deploy do teste) |
| 3 | a tela nova | `npm ci && npm run build -- --base=/novo/` em `frontend/`, num contêiner `node:22-bookworm-slim` (a função `construir_front`); a produção não precisa de Node, só o `dist` viaja | `src="/novo/assets/` no `index.html` da build |
| 4 | o pacote | o do `git archive`, mais o `vendor/` e o `dist`: nada da árvore de trabalho | sem `base-teste*.sql.gz` (as cópias anonimizadas) e sem `ensaios/` (cópias do `local.php`), que o `.gitignore` deixa fora do git e não de uma cópia de pasta |

**Quando o pacote chega ao servidor: a sua 201 (a), no começo da janela** — *"o pacote e o arquivo vão ao servidor quando
a janela começa, para uma pasta ao lado da de hoje, que o site não serve. Nada muda no servidor antes da janela; a janela
fica mais longa pelo tempo da cópia."* A pasta é `/var/www/franquiabv.com.br/public_html.novo` — ao lado da árvore de
hoje, e fora do que o nginx serve se o `root` do site for o `public/` dela (a leitura 12: se não for, a pasta muda antes
da janela) —, e o arquivo é o `local.php` do item 4 de "pronto", que vai para dentro dela, no `config/autoload/` da release
(o código antigo lê só o `config/autoload/` da árvore dele). É o passo 1 da janela. A cópia é o `scp` que o `montar`
imprime, e o desempacotar ao lado é o `levar` do `scripts/publicar-producao.sh` (com `LOCAL_PHP=` o arquivo do item 4,
que ele põe `root:<o grupo do pool>` 640 no `config/autoload/` da release); o tamanho do pacote e o tempo da cópia, o
ensaio da sua 199 (a) mede (o local, num contêiner: um pacote de cerca de 28 MB, desempacotado em menos de 1 s).

### O que tem de estar pronto antes de a janela começar

| | O quê | Onde | Sem isso |
|---|---|---|---|
| 1 | **o código que reabre as dez rotas** (a sua 189 (b)), entregue na release, com o recorte da 106 tirado junto (ele volta quando as dez fecharem), as duas guardas que não leem a sessão (ficam: a sua 208 (a)) e o colaborador da unidade inativa recusado no V2 e no V3 (a sua 226 (b)) — **o código está feito** (o interruptor `ROTAS_DO_APLICATIVO_INSTALADO` em `abertas`, desde 14/10/2026; a recusa da 226 (b), desde 15/10/2026); falta a release levá-lo | a seção acima; o item 1 de "O que falta fazer antes da janela" | a etapa 1 tira dos clientes o canal de marcar |
| 2 | **a 4c resolvida**, um par por vez | a leitura 16 | quem divide o login com outro ativo não entra |
| 3 | **a release** e o commit dela, com a migração do índice da grade, as duas rotinas das limpezas e o crontab sem o `MAILTO` já nela (os itens 2 a 4 de "O que falta") | a seção acima | a janela monta código dentro da produção |
| 4 | **o `local.php` da produção, preenchido e conferido FORA da árvore servida** | as chaves do passo 3 do dia 0 e de `config/autoload/local.php.dist` — **mas não os comandos de lá**: eles fazem `cd /var/www/franquiabv.com.br/public_html`, a árvore de hoje, e o código antigo carrega todo `config/autoload/{,*.}local.php` (o `config_glob_paths` do `eaf1fca`) no pedido seguinte. Criado ali como root, o arquivo sai `root:root` 640 e o php-fpm não o lê: a configuração não monta, e o site antigo pode cair fora de qualquer janela; legível, o código antigo passa a rodar com ele. **Pela sua 201 (a), ele é escrito fora do servidor** — nada muda lá antes da janela — e as chaves conferidas numa cópia da release fora da produção; as chaves, e o que acontece sem cada uma, estão em `scripts/verificar-local-php.php`. **Até 08/11/2026 o roteiro não servia para isso:** com as chaves do banco presentes, ele **conecta no banco do arquivo** (a "prova real", um `new PDO`) — fora da produção, com o `localhost` de lá, a conexão falhava, ele saía com 1 e parava antes de conferir as chaves indevidas; e nunca se roda com um host da produção alcançável de fora. **Desde a fatia 128 (o item 13 de "O que falta", feito)**: `php scripts/verificar-local-php.php <o arquivo> --sem-conexao` confere as chaves fatais, as graves e as indevidas sem conectar, e termina dizendo "A CONEXÃO NÃO FOI CONFERIDA (--sem-conexao)", nunca "Deploy liberado" (fora do servidor, com `USUARIO_WEB=` o próprio usuário, ou a permissão do arquivo acusa o `www-data` daquela máquina); a conexão de verdade se confere no passo 7, já no servidor, sem a opção. Ele chega ao servidor no passo 1, com o pacote, direto no `config/autoload/` da release em `public_html.novo` | o código novo não sobe: as credenciais saíram do código (**D3**) |
| 5 | **o vhost novo, escrito e conferido com `nginx -t` numa cópia** | as três `location` da tela nova de `docker/nginx/teste-host.conf.modelo` (o `= /novo`, o `^~ /novo/assets/` e o `^~ /novo/`), com a raiz da tela nova fora da árvore do código (no teste, `/var/www/<DOMINIO>/front`); **as duas negações do `migrar-senhas`** (o passo 8); **o `location ~* ^/arquivos/` que fecha a biblioteca, a primeira regex** (o D355, o passo 8); e **a manutenção** (a linha de baixo), que ele tem de carregar até o passo 13 | `/novo/` cai no ERP; o reload do passo 8 tira a manutenção e abre o site antes das conferências |
| 6 | **a página de manutenção, escrita e ensaiada** (a sua 190 (a): "deixa passar só a equipe e o próprio servidor") | os três requisitos de "1 e 2", abaixo: deixa passar a equipe **e o próprio servidor**, responde 503 a todos os outros (nunca 200), e vale igual no vhost de hoje (onde entra no passo 2), no vhost novo e na volta; os endereços da equipe são do dono (a leitura 12) | as sondas e o cron saem do servidor e batem na manutenção: com 200, um verde falso; com 503, uma volta sem defeito |
| 7 | **os dois ajustes no servidor, escritos para o de lá e aplicados na janela** (a sua 205 (a)): os três requisitos do php-fpm — os valores, o arquivo do pool das rotinas e a entrada a mais no vhost novo, que entram com o vhost e o recarregar dos passos 8 e 9; o pool do site com o teto das rotinas até o dono declarar o fim da volta (decidido em 15/10/2026, a 228 (a); o **D497**, o passo 8) —; e, se a leitura 4 der a de fábrica, **a memória do MySQL**, no valor que a memória da máquina permitir (a leitura 4), posta no começo do passo 4, antes das migrações | o item 4 de "Preparação" do procedimento (**D67**), os passos 4, 8 e 9; escrever é do repositório, aplicar é do dono, no servidor, na janela | uma rotina pendurada ocupa os processos que o aplicativo divide; e a comanda e a agenda da tela nova leem do disco (o **D493**), e as cópias grandes do passo 4 rodam com a memória de fábrica |
| 8 | **o crontab novo sem o `MAILTO`, com o aviso no log** (a sua 128 (b) e a sua 194 (a)), e a conferência dele ensaiada — **feito no repositório em 07/11/2026** (a fatia 113): falta o 8 de "O que hoje bloqueia" do procedimento no servidor, antes da janela (o `logger` e o log do sistema) | o item 4 de "O que falta" | **a janela não começa:** a sua 194 (a) pôs o crontab novo nesta etapa, e o aviso no log da sua 128 (b) só vem com ele. O recuo do passo 12 (a sua 193 (b)) é a reserva da janela, e não a falta deste item — e, se ele vier, o crontab novo entra na noite seguinte (a sua 207 (a)) |
| 9 | **o ensaio da troca e da volta, duas vezes, cronometrado, e o do passo 0 num banco descartável** (a sua 199 (a)) | "O ensaio da etapa 1", abaixo | a primeira troca de árvore é a da produção, e a janela sai sem os seus dois passos mais longos medidos |
| 10 | **o backup da janela, na forma da sua 204 (a)**: o passo 3 escrito com o comando da rotina (os quatro fatos da leitura 14, do dono), o lugar fora do servidor e fora da rotação para a cópia, e **a restauração ensaiada antes**, numa máquina que não é a produção, com o roteiro das três réplicas (que não está escrito: o **D498**) | o passo 3, abaixo; o item 8 de "O que falta" | o passo 0 roda sem o ponto de volta que a sua 191 (a) pôs antes dele ("depois do backup"), e a primeira restauração seria a de uma emergência |
| 11 | ~~as seis perguntas novas do lote~~ — **respondidas em 15/10/2026** (as 204 a 209, todas na (a)) | "As decisões do dono", no fim | — |
| 12 | **a decisão sobre a trava de marketing de outro usuário**, se a leitura 22 achar alguma no caso (b) ou no (c) dela (a sua 229 (a)) | a leitura 22, com o modo de cada trava; a decisão é do dono — o envio volta a rodar, ou fica parado —, e o que ela pede depende do caso: no (b), ficar parado pede o jeito de pará-lo no código novo, escrito com ela, antes da janela; no (c), voltar a rodar pede apagar a trava no servidor, na janela, com o envio fora de execução (o `fuser` da trava, só leitura, vazio) — uma escrita no servidor, que só entra com a decisão | no (b), o envio parado volta a rodar quando o código subir, sem ninguém ter pedido, e o cliente recebe as mensagens que ninguém esperava; no (c), o envio que o dono quer de volta segue parado, com a exceção a cada disparo |
| 13 | **"Clientes crédito" com quem compra e devolve crédito no balcão** (a sua 99 (a), o **D123**; a fatia 104, de 07/11/2026): o código novo faz comprar e devolver crédito pedirem "Clientes crédito" além do cadastro, no ERP e na tela nova. **Dar a permissão é operação de produção, e é do dono** (pela tela de permissões, antes de o código subir); nada foi dado na cópia | a lista, na réplica e só leitura: `SELECT DISTINCT v.usuario FROM vendas v JOIN usuarios u ON u.id = v.usuario JOIN grupos g ON g.id = u.grupo WHERE v.venda_credito = 1 AND u.status = 1 AND g.administrador = 0 AND v.usuario NOT IN (SELECT p.usuario FROM privilegios_usuarios p JOIN funcionalidades f ON f.id = p.funcionalidade WHERE f.nome LIKE 'Clientes cr%dito' AND p.escrever = 1);` — quem já comprou crédito e não tem a permissão (na cópia de 07/11/2026, ninguém; a pergunta de outubro contava umas 194 com o cadastro e sem ela). **Essa lista só vê a compra**: a devolução, no código de antes do corte, só baixa o saldo e não grava quem a operou (o D179; a `clientes_creditos_devolucoes` chega no passo 0), e quem só devolve não aparece nela. Por isso, a segunda, também só leitura, lista todos os que hoje compram ou devolvem pelo cadastro e perderiam as duas: `SELECT u.id FROM usuarios u JOIN grupos g ON g.id = u.grupo WHERE u.status = 1 AND g.administrador = 0 AND u.id IN (SELECT p.usuario FROM privilegios_usuarios p JOIN funcionalidades f ON f.id = p.funcionalidade WHERE f.nome = 'Clientes cadastro' AND p.escrever = 1) AND u.id NOT IN (SELECT p.usuario FROM privilegios_usuarios p JOIN funcionalidades f ON f.id = p.funcionalidade WHERE f.nome LIKE 'Clientes cr%dito' AND p.escrever = 1);` (na cópia de 07/11/2026, 6 pessoas; nenhum grupo com `administrador` nulo, que as duas perderiam) | quem compra crédito no balcão sem a permissão recebe "página não encontrada" no ERP e 403 na tela nova, no primeiro atendimento depois da janela |

**O `local.php` desta etapa** é o do MySQL, e é ele que a etapa 2 guardará no passo 7 dela. Além das chaves que o passo 3
do dia 0 pede:

- **`cron.token`: a sua 193 (b), "O token das rotinas na janela: junto com o crontab novo"** — um valor novo, sorteado
  sem imprimir por `sh scripts/sortear-token-cron.sh <o caminho do arquivo>` no arquivo preparado fora do servidor (a
  rotação da linha 9 do passo 5 do dia 0, junto com a troca de forma do passo 9 do procedimento, **D331**), e que passa a
  valer no passo 12, quando o crontab novo o leva. O código antigo não o lê: ele compara com o valor escrito nele, e o
  crontab de antes continua servindo a ele numa volta. **E, "se o crontab novo não puder entrar", o token de hoje** — a
  mesma 193 (b) —, para o crontab de antes servir também ao código novo, que aceita o `?token=` (**D4**): sorteado à espera
  do crontab novo, e sem ele no passo 12, as rotinas dariam 401 caladas — o recuo do passo 12 confere isso antes de
  instalar. Por isso o valor só se sorteia com o crontab novo pronto e ensaiado (o item 4 de "O que falta"): o que o
  impedia era o endereço do `MAILTO`, e a sua 128 (b) tirou o e-mail; e a conferência do passo 9, que ainda exigia um
  `MAILTO` com endereço, deixou de exigi-lo em 07/11/2026 (a fatia 113) — o crontab novo está pronto e ensaiado no
  repositório, e o valor se sorteia no preparo;
- **`asaas.api_key` e as outras credenciais do D3: os valores de hoje** — a sua 193 (b), "as outras oito depois de você
  declarar que não se volta mais ao código antigo. Até lá, o arquivo nasce com os valores de hoje". O dia 0 pedia a chave
  do Asaas rotacionada, "nunca a antiga"; pela 193 (b), a antiga vale por alguns dias, com a mesma exposição de hoje. A
  rotação é do dono, depois da declaração da sua 198 (a) — com ela, uma volta ao código antigo, que traz as chaves velhas
  escritas, perde o Asaas, o push e o e-mail, e com a senha do MySQL trocada ele nem conecta;
- **`whatsapp.central`**: o número novo da franqueadora (a resposta 169); vazio, o login V4 não manda código, e o
  aplicativo instalado não sente;
- **`push.provedor`** fica `onesignal` (o padrão); o FCM e o `ambos` são do aplicativo novo;
- **`sessao.cookie_secure`** ligado: a produção é HTTPS (`config/autoload/sessao.global.php`; o `verificar-local-php.php`
  não a confere);
- **ausentes**: `view_manager.display_exceptions` e `display_not_found_reason` (presentes, o verificador sai com 1, **D16**)
  e `busca.tags_sem_acento` e `busca.campanhas_sem_acento` (as duas são do 7c, da etapa 2, na mesma linha; a das
  campanhas, o **D618**, nasce desligada e não depende de passada).

---

## O que falta fazer antes da janela

Cada item é o que uma decisão do dono pede e **o repositório ainda não tem** — conferido com `grep` e `ls` em 14/10/2026,
sobre o `main` de `f2a9b7e`. "Código" é uma rodada, com teste e portão; "escrita" é este documento ou o procedimento; "do
dono" é no servidor, no BVIP ou numa resposta. A janela não começa sem eles — menos a rotação das oito credenciais, que
vem depois da declaração da sua 198 (a) (o item 5). O crontab novo (o item 4) não é exceção: a sua 194 (a) o pôs nesta
etapa, e o recuo da sua 193 (b) é a reserva da janela, para o crontab novo que não entrar no passo 12 (e, depois de um
recuo, o crontab novo entra na noite seguinte: a sua 207 (a)). **As seis respostas de 15/10/2026 (204 a 209) e a 226 estão
nos itens:** a 226 (b) e a 208 (a) no 1, a 207 (a) no 4, a 204 (a) no 8, a 206 (a) no 9 e a 205 (a) no 14; a 209 (a) não
pede nada antes da janela (a linha do tempo). **E as duas da verificação do mesmo dia:** a 228 (a) no 14 (o teto curto do
site só depois do fim da volta), e a 229 (a) numa leitura, a 22, e no item 12 de "pronto". **E as três das telas que faltam, de 04/11/2026** (a fatia 76u): a
294 (a) no 15, a 295 (a) no 16 e a 298 (c) no 17. **E as duas do recálculo da pontuação** (a fatia 106, de 07/11/2026): a 230 (a) e a 123 (a) no 18.

| | o quê | a decisão | o que o repositório tem hoje | o débito | de quem |
|---|---|---|---|---|---|
| 1 | **reabrir as dez rotas ao aplicativo instalado**, sem a sessão do ERP, com o recorte da 106 tirado junto; as duas guardas que não leem a sessão (ficam: a sua 208 (a)); o colaborador da unidade inativa no V2 e no V3 (recusado: a sua 226 (b)); os snapshots que gravaram o 401; o `scripts/ensaiar-escritas-do-aplicativo.php`; a conferência, na cópia, do serviço de outra unidade; e o fechar, depois, como outra entrega | a sua 189 (b); a sua 106 (a); a sua 158 (a) | **feito em 14/10/2026** (AS-DEZ-ROTAS-ABERTAS; o commit desta entrega): as dez respondem sem sessão, com o `Access-Control-Allow-Origin: *` de julho em nove, atrás da constante `AplicativoController::ROTAS_DO_APLICATIVO_INSTALADO` (`abertas`); o recorte da 106 só com elas fechadas; os snapshots dos dois estados, e a suíte de contrato escolhe pela constante; o ensaio das escritas sem sessão, de outra origem; a conferência na cópia: **0 dos 71.402** atendimentos de 2026 pelo aplicativo com o serviço de outra unidade. **E as duas respostas, em 15/10/2026:** a 208 (a), as duas guardas ficam (nada mudou no código, e a contagem deu zero: nada volta ao dono); e a 226 (b), o V2 e o V3 recusam o colaborador ativo de unidade inativa com "Informe o colaborador.", nos dois estados do interruptor (`AplicativoController::semOColaboradorDaUnidadeInativa`, RESPOSTAS-DE-15-10). **Falta só o fechar** — trocar a constante para `fechadas` —, quando o aplicativo novo estiver em campo, medido com a constante trocada: 74 de 74 no contrato | **D91** (e o **D59**, o **D237**), os três ANOTADOS pela 189 (b); o **D482**, corrigido pela 226 (b) | código, feito; o fechar, outra entrega |
| 2 | **a migração do índice da grade** — o `FK_AGENDAS_colaborador` trocado, com o mesmo nome, por `agendas (colaborador, data)` (era o `IX_AGENDAS_COLABORADOR_DATA` ao lado; o mesmo nome é o que as cópias e o gerador já tinham) —, com a linha `\| 0 \|` e o laço do procedimento, o laço do ensaio geral e o `mysql.sql` juntos (o portão do passo 0 exige), as duas cópias locais, e o gerador do PostgreSQL conferido contra o `mysql.sql` novo | a sua 126 (a), a sua 196 (b) e a sua 83 (a) | **feita em 07/11/2026 (a fatia 114, ESQUEMA-QUE-SO-ACRESCENTA):** a `scripts/migracao-agendas-indice-colaborador-data.sql`, no molde da das vendas por cliente, na linha `\| 0 \|` e no laço do passo 0, no laço do ensaio geral e com o `KEY` de duas colunas no `mysql.sql` (o portão do passo 0 saiu 0 e cobra as colunas; o caso `PassoZeroDoEsquemaQueSoAcrescentaTest`); o `postgres/schema.sql` regerado não mudou nesse índice (o gerador já o ampliava); as duas cópias já o tinham desde 02/10/2026, e nelas ela diz "indice ja ampliado". Falta só rodá-la na produção, pelo laço, e ler o `innodb_buffer_pool_size` de lá | **D189** (o portão, **D323**) | o laço do passo 0; a base do teste, na próxima cópia exportada, do dono |
| 3 | **as duas rotinas das limpezas** — a da fila, de 24 horas, só na pendente, e a do arquivo de exclusões, de 60 dias —, com rota, teste e as linhas em `crontab-a-aplicar.md`; e o `ALTER EVENT limpar_push_app_24h DISABLE` e o `ENABLE`, ensaiados. **A linha da fila roda ao menos uma vez depois de as filas do dia anterior completarem 24 horas e antes do envio das 10:00** — as do parabéns, das 06:00, e as do "sentimos sua falta", das 07:00, cada rotina com até 10 minutos; de hora em hora, como o evento que ela substitui (aos 35 minutos), cumpre —: a sobra tem de sair antes do envio das 10:00 do dia seguinte, como hoje (o **D489**: "Com a rotina da 127, fica como hoje"); uma rodada só por dia antes das 06:00 a deixaria entrar nesse envio, um dia atrasada, e a 186 (c) deixaria de ser "como está". A do arquivo, uma vez por dia, como o evento dela | a sua 127 (a), a sua 195 (a), a sua 184 (a), a sua 185 (a); a linha do parabéns não muda (a sua 186 (c)) | **feito em 07/11/2026** (a fatia 113, ROTINAS-NOVAS-E-CRONTAB): `/api/cronjobs/pushAppLimpar` (a pendente de mais de 24 h, a falhada poupada: `PushAppRepository::apagarPendentesAntigas`) e `/api/cronjobs/agendasExclusoesLimpar` (60 dias pela `data_exclusao`), no molde das outras — o token e o 401, a trava, o log das rotinas —, em lotes de 5.000; as linhas em `crontab-a-aplicar.md`, aos 35 minutos de cada hora e às 02:40, com o `logger`; o `tests/Caracterizacao/D26LimpezasNoCrontabTest.php` nos dois bancos (a fronteira dos dois prazos, a falhada poupada, os lotes, a trava). **O `ALTER EVENT`, medido num banco descartável** (o MySQL 8.4.13 local, 07/11/2026): o `DISABLE` e o `ENABLE` mudam o estado, **e trocam o `DEFINER` pela conta que os roda** (`franquiabv@%` virou `root@localhost`); com `ALTER DEFINER=<o da leitura 5> EVENT ...`, o `DEFINER` fica — é a forma do passo 12 e da volta. Falta o ensaio na produção ou no BVIP (o item 11) | **D26** (e o **D478**) | código, feito; o evento, ensaio (o item 11) |
| 4 | **o crontab sem o `MAILTO`, com o aviso no log do sistema**: a forma do aviso em cada linha; o primeiro bloco do passo 9 sem o `MAILTO=` e a conferência do segundo sem exigir o endereço, e conferindo o aviso; o domínio raiz no bloco; o 8 de "O que hoje bloqueia" para a etapa 2; e o `scripts/ensaiar-crontab-do-passo-9.sh` junto. Os mesmos dois blocos servem à noite seguinte de um recuo (a sua 207 (a); o passo 12) | a sua 128 (b), a sua 194 (a), a sua 5 (b), a sua 207 (a) | **feito em 07/11/2026** (a fatia 113): o primeiro bloco do passo 9 escreve `MAILTO=""` e põe em cada linha o `\|\| logger -t rotinas "falhou (curl $?): <a rota>"` e o domínio raiz, e na da apuração dos royalties guarda a resposta e manda o `aviso` dela ao log (o **D364**); a conferência do segundo exige o aviso, o domínio raiz, nenhum `%`, nenhum `MAILTO` com endereço e uma linha dos royalties com o aviso; o 8 de "O que hoje bloqueia" testa o `logger` e o log do sistema; as linhas de `crontab-a-aplicar.md` na mesma forma; e o `scripts/ensaiar-crontab-do-passo-9.sh` com os casos novos (a correção à mão sem o `logger`, com outra marca, no `www`, com `%`, com o `MAILTO`, os royalties sem o aviso e duas vezes) e as linhas rodadas com um `curl` e um `logger` de mentira, no Mac (bash e gawk) e no contêiner (dash e mawk). Falta o 8, no servidor | **D331** (e o **D364**) | código e escrita, feitos; o 8, do dono, no servidor |
| 5 | **o `cron.token` novo na janela, e as outras oito credenciais depois do fim da volta** | a sua 193 (b), a sua 198 (a) | o `scripts/sortear-token-cron.sh` existe; o `local.php` com os valores de hoje está escrito aqui ("O `local.php` desta etapa"); a rotação das oito é a do passo 5 do dia 0 | **D3**, **D331** | escrita (feita aqui); a rotação, do dono, depois da declaração |
| 6 | **a linha que anula a cópia antiga das senhas, ensaiada**, e o bloqueio nas duas formas, reposto na volta | a sua 202 (b) | a linha está no passo 6, sobre a `usuarios.senha_php83` (anulável; a rota só escolhe `IS NOT NULL`); o `scripts/bloquear-migrar-senhas.sh` fecha só a `location =`, e a negação do `/index.php/` é à mão, no vhost novo e na volta; nada ensaiado | **D2**, **D79** (o esquema, **D78**) | escrita (feita aqui); o ensaio, no item 11 |
| 7 | **a linha que desfaz o Líder, ensaiada** | a sua 192 (a), a sua 11 (b) | a linha está no passo 6, com a condição do `SELECT` de antes; nada ensaiado | **D124** | escrita (feita aqui); o ensaio, no item 11 |
| 8 | **o backup da janela, na forma da sua 204 (a)**: o dump da rotina, disparado à mão no passo 3 — o passo escrito com o comando dela, sem mudar a rotina —; a rotina de hora em hora parada no passo 1 e religada no 12 e na volta; a cópia fora da rotação e fora do servidor, guardada até o fim da volta; e a restauração — o roteiro dela num primário com três réplicas, e o ensaio numa máquina que não é a produção | a sua 204 (a); a sua 197 (a descrição), a sua 191 (a), a sua 198 (a), a sua 202 (b) | nada no repositório copia a produção; o **D33** nomeia o `dumpv3.sh`; o `scripts/ensaiar-corte.sh` e o `scripts/exportar-base-teste.sh` têm a forma do `mysqldump`; o `operacao-atual.md`, "Backups", está [A CONFIRMAR]; os quatro fatos da pergunta 204 não vieram com a resposta; o roteiro da restauração com as três réplicas não está escrito | **D498** (registrado em 15/10/2026) | do dono: os quatro fatos (a leitura 14), o destino de fora do servidor, e o ensaio da restauração; escrita: o passo 3 com o comando, e o roteiro da restauração |
| 9 | **o roteiro de publicar na produção**: monta a release fora (o `git archive`, o `composer`, a build com `--base=/novo/`), leva o pacote e o `local.php` à pasta ao lado no começo da janela, troca as árvores e as pastas, e volta à liberação anterior — o da janela e o das entregas do dia seguinte, que saem **à noite, como a janela** (a sua 206 (a)): depois das 00:05 e antes das rotinas da 01:10, com a página de manutenção, o cron parado e a espera das rotinas em curso — e o ensaio dele dá o tempo da entrega e o da volta à liberação anterior, que fixa a hora-limite de cada entrega (01:10 menos a volta; "Depois da janela") | a sua 200 (c), a sua 201 (a), a sua 206 (a) | **escrito em 08/11/2026, e nunca rodado em servidor** (a fatia 129): `scripts/publicar-producao.sh` -- o `montar` na sua máquina; no servidor, o `levar`, o `trocar` (o da janela: para antes do PHP-FPM), o `entregar` (o das entregas: troca, recarrega e confere) e o `voltar`. **Quem recusa o quê** (a verificação da fatia 129): o `trocar`, o `entregar` e o `conferir` recusam fora da hora da 206 (a), sem a manutenção no `nginx -T`, com o cron ativo, sem o crontab desta noite guardado (`CRONTAB_GUARDADO`, sem padrão: numa entrega, nunca o `/root/crontab-antes-do-corte.txt` da janela), com rotina em curso (a espera do passo 2, contando o usuário do `local.php` da release, o do que está no ar e, com o código antigo no ar, o `USUARIO_DB_NO_AR`, a leitura 10) ou com a produção divergente da impressão digital (o **D37**); o `levar` só confere a hora e o pacote (é o passo 1, antes da manutenção do 2); o `voltar` confere a manutenção, o cron e as rotinas, sem hora nem impressão (é o caminho de sair), e, na volta ao código antigo, o bloqueio do `migrar-senhas` no `nginx -T` (o item 2 de "A volta"). **Ele recusa sem a página de manutenção** (o item 10, que não existe). O ensaio local (`scripts/ensaiar-publicar-producao.sh`, contêiner descartável, dublês do nginx, do cron, do MySQL e do PHP-FPM) deu a entrega em menos de 1 s e a volta em menos de 0,1 s -- trocas de nome; não são os números da produção | o **D37** (PARCIAL) | rodá-lo, e o ensaio no BVIP que dá a entrega e a volta de verdade (a sua 199 (a)): do dono ou de quem ele indicar; os números `ENTREGA_MIN` e `VOLTA_MIN` que ele pede saem desse ensaio |
| 10 | **a página de manutenção** que deixa passar só a equipe e o próprio servidor, responde 503 aos outros e sobrevive à troca do vhost, escrita e ensaiada | a sua 190 (a) | não existe: nada em `docker/nginx/` nem em `scripts/` a faz | a registrar | escrita (o vhost); os endereços da equipe e o pôr no servidor, do dono |
| 11 | **o ensaio da etapa 1 no BVIP**: o roteiro — a troca, a volta, o passo 0 no banco descartável, as duas linhas do passo 6, o evento, a venda de teste —, duas vezes, cronometrado | a sua 199 (a) | não existe; o `ensaio-geral-do-corte.md` é o da etapa 2, e o `scripts/ensaiar-corte.sh` também | a registrar | escrita (o roteiro); rodá-lo, e montar e apagar o descartável, do dono no BVIP |
| 12 | **a venda e o aviso de teste**: a unidade combinada, o cadastro de quem testa nela (já com uma venda finalizada, ou sem indicação à espera), o caixa dele, o aparelho dele, e a medida do que a venda cancelada deixa no retrato, na comissão e nos pontos — e do que o cancelar não desfaz: a comissão da indicação, o cupom do sorteio e o do checkout | a sua 203 (a) | o passo 11 a escreve; a medida não existe | a registrar | escrita (feita aqui); a unidade, o cadastro e o aparelho, do dono; a medida, no ensaio |
| 13 | **uma conferência do `local.php` fora do servidor que não conecte** | a sua 201 (a) | **feito em 08/11/2026** (a fatia 128, o **D329**): o `scripts/verificar-local-php.php <arquivo> --sem-conexao` confere as chaves fatais, as graves e as indevidas sem conectar, diz "CONEXÃO NÃO CONFERIDA" e termina com "A CONEXÃO NÃO FOI CONFERIDA (--sem-conexao)" no lugar do "Deploy liberado"; o `D329VerificarConectaPeloDriverTest` o roda nos dois bancos. Sem a opção, ele conecta no banco do driver declarado, e sem a extensão do driver sai 1 | **D329** | código, feito; rodá-lo sobre o `local.php` de produção, fora do servidor, de quem o escreve |
| 14 | **os dois ajustes no servidor, escritos para o de lá, para entrar na janela**: o pool das rotinas (um arquivo do php-fpm), a entrada a mais no vhost novo que manda as rotinas a ele, o teto e os processos (os três requisitos) — **com o pool do site no teto das rotinas até o dono declarar o fim da volta** (decidido em 15/10/2026, a 228 (a); o **D497**), e não no teto curto (o passo 8), que entra depois da declaração, numa entrega à noite ("Depois da janela") —; e o valor da memória do MySQL, se a leitura 4 der a de fábrica, com a forma de pôr (a medir: o MySQL 8 a muda sem reiniciar) | a sua 205 (a); a sua 228 (a), o teto curto do site só depois do fim da volta (o **D497**) | os três requisitos estão no item 4 de "Preparação" do procedimento, sem os valores do servidor de lá; os moldes do teste são o `docker/php/fpm-teste.conf` (o pool, 16 processos) e o `docker/mysql/teste.cnf` (4 GB, "não vale para a produção") | **D67**, **D493** (o **D189**), **D497** | escrita, com as leituras 4 e 7 na mão; aplicar na janela, do dono |
| 15 | **a migração do índice das vendas por cliente** — o `FK_VENDAS_cliente` trocado, com o mesmo nome, por `vendas (cliente, status, data_criacao)` (o passo 4) —, com a linha `\| 0 \|` e o laço do procedimento, o laço do ensaio geral e o `mysql.sql` juntos (o portão do passo 0 exige), e o gerador do PostgreSQL conferido contra o `mysql.sql` novo | a sua 294 (a), que muda a decisão do D308 | **feita em 04/11/2026 (a fatia 76b, a CA1 da Carteira):** a `scripts/migracao-vendas-indice-cliente-status-data.sql`, na linha `\| 0 \|` e no laço do passo 0, no laço do ensaio geral e com o `KEY` de três colunas no `mysql.sql` (o portão do passo 0 saiu 0, e cobra também as colunas do índice que o `mysql.sql` tem com o mesmo nome); o `postgres/schema.sql` regerado é o mesmo; ensaiada num banco descartável (2,3 s; a segunda vez sem efeito). Falta só rodá-la na produção, pelo laço | **D308** (e o **D697**) | o laço do passo 0 |
| 16 | **o roteiro do consumo refeito uma vez** — a contagem antes, o `UPDATE` de `clientes.consumo` para a soma das vendas finalizadas onde a coluna é nula ou difere em mais de 1 centavo (o nulo de quem nunca comprou a zero, a sua 303 (a)), e a contagem depois (o passo 7) —, ensaiado num banco descartável, e a rotina da 01:10 refazendo também quem teve venda cancelada, no código desta etapa | a sua 295 (a) | **feito em 04/11/2026** (a CA7 da Carteira, a fatia 76o): o `scripts/refazer-consumo-dos-clientes.sql`, com a contagem antes, a lista com o número de antes, o `UPDATE` pela régua de 1 centavo e a conferência; a rotina junta aos que vieram os clientes ativos com uma venda de `cancelado_em` nos dois últimos dias (`ClienteRepository::selecionarClientesComVendaCanceladaNoPeriodo`); o `tests/Caracterizacao/ConsumoDoClienteTest.php` nos dois bancos (o roteiro só no MySQL), e o roteiro num clone descartável da cópia | **D698** | código, feito; o `UPDATE`, no passo 7; o ensaio na cópia, no item 11 |
| 17 | **o roteiro das quatro permissões com o nome da tela** — um `UPDATE` só de `funcionalidades`, pelo id e pelo nome antigo das quatro, parando antes dele se um nome novo já existir em outro id, e depois dele, dentro da transação, se um dos quatro ids não ficar com o nome novo; e o inverso para a volta (o passo 7 e o item 4 da volta) —, as fixtures dos testes cedendo também pelo nome antigo (para o clone do ensaio não ganhar o nome repetido), e o `SessaoAcl` do código desta etapa aceitando o nome novo e o antigo | a sua 298 (c) | **feito em 04/11/2026** (a DE3 das Datas da equipe, a fatia 76n): o `SessaoAcl::salvarAcessoPeloNomeNovo` pergunta pelo nome novo e, sem ele, pelo antigo; o `scripts/renomear-permissoes-das-telas-novas.sql` e o inverso, `scripts/devolver-nomes-das-permissoes-das-telas-novas.sql`, com as duas paradas; as fixtures com o nome novo e cedendo também pelo antigo; o `scripts/ensaiar-permissoes-com-o-nome-da-tela.php` no `conferir-tudo.sh`. As cópias locais seguem com o nome antigo | **D701** | código, feito; o `UPDATE`, no passo 7; o ensaio na cópia, no item 11 |
| 18 | **o roteiro da pontuação recalculada uma vez** — a linha de saldo zerada de cada par com pontos `ativa` e sem saldo, e depois a soma das linhas `ativa` em toda linha de saldo que difere dela, com a contagem antes, as listas e a conferência (o passo 7) —, ensaiado num banco descartável, e o irmão do PostgreSQL para o 7d da etapa 2 | as suas 230 (a) e 123 (a) | **feito em 07/11/2026** (a fatia 106, RECALCULO-DA-PONTUACAO-NO-CORTE): o `scripts/recalcular-pontuacao-no-corte.sql` e o `postgres/recalcular-pontuacao-no-corte.sql`, o `RecalculoDaPontuacaoNoCorteTest` nos dois bancos e um clone descartável das duas tabelas da cópia em cada banco; não ensaiado na janela | **D284**, **D312** | rodá-lo, do dono, na janela; o ensaio, no item 11 |

**E os de antes, que seguem:** as vinte e duas leituras de "O que se lê na produção" (do dono, só leitura); a 4c, um par
por vez (do dono, o **D349**); os três requisitos do php-fpm (do dono, o **D67**; na janela, pela sua 205 (a): o item 14); o
vhost novo, escrito e conferido numa cópia (o item 5 de "pronto"); e o `local.php` preenchido e conferido fora do servidor
(o item 4). As seis perguntas do lote (CORTE-ETAPA-1-ESCOLHAS) foram respondidas em 15/10/2026, **e as duas da
verificação do mesmo dia também, na opção (a)**: o teto curto do pool do site contra a volta que não desfaz o php-fpm (o
**D497**) — decidido em 15/10/2026, a 228 (a): até o dono declarar o fim da volta, o pool do site fica com o teto das
rotinas (o passo 8), e o teto curto entra depois, numa entrega à noite —; e a rotina parada no servidor por uma trava de
outro usuário (o **D499**) — decidido em 15/10/2026, a 229 (a): a listagem das travas é a leitura 22, e a decisão sobre a
trava de marketing que ela achar, o item 12 de "pronto".

---

## A janela

A ordem importa mais que a velocidade, como no procedimento. Cada passo tem o comando, o sinal de que deu certo e a volta
**até ali**. Os passos que o `procedimento-de-corte.md` já escreve **não estão copiados**: a linha diz qual é, e o texto de
lá vale como está — duas cópias de um mesmo comando divergem no primeiro dia em que alguém corrigir só uma, e os de lá
têm portão (ver "Os portões", no fim). O banco é o `franquia_producao` em todos os comandos.

| # | Passo | Comando | Confere | Volta até aqui |
|---|---|---|---|---|
| 1 | parar o cron, e trazer a release | depois das 00:05 (a sua 190 (a)): os dois primeiros comandos do passo 1 do procedimento — o `crontab -l` guardado em `/root/crontab-antes-do-corte.txt` e o `crontab -r` —, e os outros agendadores que a leitura 11 achar, guardados do mesmo jeito; a rotina do dump de hora em hora, parada junto com o cron e guardada como eles (a sua 204 (a); o passo 3); e, enquanto o 2 espera, a cópia do pacote para `public_html.novo` e do `local.php` para o `config/autoload/` dela (a sua 201 (a); o `levar` do `scripts/publicar-producao.sh`, com `HORA_LIMITE=05:00`) | nada: o `pgrep` do procedimento não vê rotina nenhuma (abaixo), e quem diz que acabaram é o 2; da release, o `verificar-local-php.php` do passo 7 | `crontab /root/crontab-antes-do-corte.txt`, e os outros agendadores e a rotina do dump, cada um como foi guardado; a pasta ao lado fica, e o site não a serve |
| 2 | parar a aplicação | a página de manutenção (abaixo) — **só ela**: parar o PHP, a outra forma do passo 2 do procedimento, impede os passos 10 a 12 | a manutenção responde 503 de fora; e, esperando o que for preciso, o `SHOW PROCESSLIST` sem conexão nenhuma do usuário da aplicação, em `Query` **nem em `Sleep`** — **e sem o dump de hora em hora que já disparou** (abaixo; a sua 204 (a)) | tirar a manutenção |
| 3 | o backup | o dump da rotina, **disparado à mão** (a sua 204 (a)), com o sistema parado e antes do passo 0 (a sua 191 (a)), a posição do log binário e a cópia do vhost, já com a manutenção e ainda sem o bloqueio (abaixo) | o dump inteiro, conferido, e copiado para fora da rotação e fora do servidor | — |
| 4 | o passo 0 | **antes do laço, a memória do MySQL**, se a leitura 4 deu a de fábrica (a sua 205 (a); abaixo); e o laço de "O que hoje bloqueia" do procedimento, **copiado de lá na hora** — com a migração do índice da grade nele, quando ela existir (a sua 126 (a) e a sua 196 (b); abaixo) | cada migração diz o que fez, e o laço para na primeira que falhar; e as três réplicas, sem erro (abaixo) | nada a desfazer: o código antigo não lê nem escreve o que elas criam |
| 5 | a semente da vigência | `mysql franquia_producao < scripts/semente-usuarios-horarios.mysql.sql` | a última consulta dá 0 | nada: a tabela fica, e o código antigo não a lê |
| 6 | o Líder, e a cópia antiga das senhas | o Líder (a sua 192 (a)): o `SELECT` do cabeçalho de `scripts/migracao-grupo-lider-gerente.sql`, e então `mysql franquia_producao < scripts/migracao-grupo-lider-gerente.sql`; a cópia (a sua 202 (b)): a contagem e a linha que a anula (abaixo) | uma linha mudada no Líder: o grupo 8 com `gerente` em 1; e a contagem da cópia em zero depois | o inverso do Líder (abaixo); a cópia, só do backup — e a volta não precisa dela |
| 7 | trocar o código | as duas trocas de nome, e o que passa da árvore antiga para a nova (abaixo); com o código novo no lugar, as linhas de `colunas_gravadas`, os pacotes de venda cancelada, o consumo dos clientes refeito (`scripts/refazer-consumo-dos-clientes.sql`, a sua 295 (a)), a pontuação de todos recalculada pelo histórico (`scripts/recalcular-pontuacao-no-corte.sql`, as suas 230 (a) e 123 (a)) e as quatro permissões com o nome da tela (`scripts/renomear-permissoes-das-telas-novas.sql`, a sua 298 (c)), cada roteiro pelo caminho da árvore no lugar (abaixo) | `php /var/www/franquiabv.com.br/public_html/scripts/verificar-local-php.php` sai com 0, e a lista do `grave` foi lida; cada roteiro diz "o mysql saiu com 0" (o `PIPESTATUS` do `mysql`, e não o `$?` do `tee`), a contagem do consumo foi anotada, o `MUDOU` das permissões diz 4, o `DEPOIS` tem as contagens do `ANTES` e a linha dos nomes antigos diz 0 | as duas trocas ao contrário, e os quatro nomes de volta (`devolver-nomes-das-permissoes-das-telas-novas.sql`, pelo caminho da árvore nova, `public_html.novo-<data>/scripts/`) antes de o código antigo servir: com a manutenção ainda no ar (o item 4 da volta) |
| 8 | a tela nova e o nginx | o `dist` na raiz da tela nova; o vhost novo (com a manutenção, as duas negações do `migrar-senhas` e a entrada das rotinas para o pool delas, a sua 205 (a)); o arquivo do pool das rotinas e os valores do php-fpm, que o 9 põe no ar; `sh /var/www/franquiabv.com.br/public_html/scripts/bloquear-migrar-senhas.sh --aplicar` (o diretório ainda é o da troca do 7, onde `scripts/` não existe), que diz que o bloqueio já está, ou o põe; `nginx -t && systemctl reload nginx` | o `nginx -t` passa, e o `nginx -T` mostra as duas negações e a manutenção — **nunca um `curl` na rota** | a cópia do vhost de volta, com o bloqueio reposto (como na volta) |
| 9 | recarregar o PHP-FPM | o nome do serviço é a medir; o recarregar traz o pool das rotinas e os três requisitos (a sua 205 (a)) | nenhum pedido em curso antes (o 2); depois, o pool das rotinas de pé (a leitura 7, de novo) | — (a volta não desfaz o php-fpm: com o vhost de antes, as rotinas voltam ao pool de todos — que por isso fica com o teto delas até o fim da volta: decidido em 15/10/2026, a 228 (a), o **D497**, o passo 8) |
| 10 | conferir sem escrever | abaixo, de dentro da manutenção | o 401, a vigência, o `/novo/` com a build | o 8 e o 7 ao contrário |
| 11 | o teste à mão | abaixo: o dono ou quem ele indicar, com um usuário de verdade de cada papel (a sua 190 (a)) | tudo passa — **qualquer falha é volta**; a venda de teste cancelada, com o motivo "teste do corte" (a sua 203 (a)) | "A volta", abaixo |
| 12 | religar o cron | o crontab novo (a sua 194 (a)): os dois blocos do passo 9 do procedimento **depois de a conferência deixar de exigir o `MAILTO` com endereço** (a sua 128 (b); o item 4 de "O que falta"), com o domínio raiz; as linhas de `crontab-a-aplicar.md` — a dos convênios e as das duas limpezas (a sua 195 (a)) —; o evento de hora em hora desligado; os outros agendadores e a rotina do dump que o passo 1 parou, de volta (abaixo); e a conta dos 10 minutos — ou, se o crontab novo não puder entrar, o recuo (abaixo; a sua 193 (b)), e o crontab novo na noite seguinte (a sua 207 (a)) | só `" 200 "`, e mais de zero; o `limpar_push_app_24h` em `DISABLED` | "A volta", abaixo |
| 13 | liberar | tirar a página de manutenção, antes das 05:00 (a sua 190 (a): "se às 5h não estiver liberado, voltamos"); se o crontab só voltou depois das 05:25 — na janela ou na volta —, o retrato dos royalties à mão, no mesmo dia, e na noite de 15 para 16 antes das 09:00 (a sua 209 (a); a linha do tempo, que diz como saber se foi pulado) | as três réplicas, de novo (o passo 4) | "A volta", abaixo |

### 1 e 2. Parar o cron e a aplicação

Os comandos vêm do procedimento, menos o `pgrep` e o "parar o PHP" (abaixo), e o motivo é outro. Lá, a rotina que dispara depois do dump grava um dado que nunca
chega ao PostgreSQL; aqui não há dump, e o que pesa é que a rotina que rodar durante a troca roda metade num código e
metade no outro, e que os três `ALTER` que copiam a tabela (o passo 4) seguram a escrita em vendas, nos pontos e nos
cupons enquanto copiam. O crontab guardado é a entrada do passo 12 e da volta. O aplicativo dos clientes fica fora junto
com a aplicação.

**A rotina não é um processo que o `pgrep` veja.** As entradas do crontab são `curl` para HTTPS (`f1-6-agendamento.md`), e
a rotina roda dentro de um processo do php-fpm ("Os crons não são scripts de linha de comando: são rotas HTTP",
`operacao-atual.md`). O `pgrep -af 'php .*public/index.php'` do passo 1 do procedimento só casa com um PHP de linha de
comando: ele imprime "nenhuma rotina em curso" com uma rodando. E o `crontab -r` não para o que já disparou — o
`marketing/envios` tem 590 s, e o worker continua depois de o `curl` desistir (o **D67**). Por isso o sinal é do passo 2:
com a manutenção no ar e o cron parado, **nenhuma conexão do usuário da aplicação no `SHOW PROCESSLIST`, em `Query` nem em
`Sleep`** — a conexão do PHP vive o pedido inteiro, inclusive o tempo parado no Asaas ou no WhatsApp, em que ela fica em
`Sleep` (se a aplicação não usa conexão persistente: a leitura 7; as conexões de outros usuários, como a réplica, não
contam). E, se o pool tiver a página de status (a leitura 7), nenhum pedido de `/api/cronjobs`, `/api/marketing` ou
`/api/cupons` nela. Espere o sinal, e não o relógio. Sem isso, o `ALTER` que copia `vendas` espera a trava de metadados da
rotina, o recarregar do passo 9 pode matá-la no meio (o `process_control_timeout`), e ela termina metade num código e
metade no outro.

**E o dump de hora em hora que já disparou** (a sua 204 (a): a rotina dele "parada junto com o cron no começo da
janela"). O passo 1 para os próximos disparos, e não o que já está rodando — o mesmo do `crontab -r` com as rotinas —, e
o sinal de cima não o vê: ele roda com outro usuário do MySQL, e não pelo php-fpm. Um dump de 2,3 GB ou mais leva
minutos, e o que disparou no começo da hora pode estar no meio quando a janela começa: o passo 3 dispararia outro junto
(e, se o arquivo da rotina leva o nome da hora, os dois escrevem no mesmo), e o passo 4 o pegaria aberto — o `ALTER` que
copia `vendas` espera por ele, ou ele falha com "Table definition has changed" (o passo 3). Por isso o passo 2 espera
também **nenhum dump em curso**: `pgrep -af '[m]ysqldump|[d]umpv3'` vazio no servidor (os colchetes impedem que o padrão
case com a própria linha, quando ela roda por um `ssh` ou um `sh -c`; o nome do processo, pela leitura 11), e
nenhuma conexão do usuário do dump no `SHOW PROCESSLIST` (o usuário, pela mesma leitura — ela cobre o dump que rode de
outra máquina). Só leitura, e nada se mata: espera-se o fim dele, e o arquivo que ele deixar é uma rodada inteira. **Não
ensaiado:** o dump de hora em hora não roda no BVIP, e o `pgrep` não está no contêiner da aplicação. **Por isso a janela começa depois das 00:05** (a sua 190 (a): "uma noite de dia útil, fora dos dias 15 e
16 e da virada do mês"): o `yungasFaturamentoAtualizar` das 23:40, o `fidelizacaoAtualizar` das 23:50 (que só trabalha
com "hoje", e não refaz o dia perdido) e o `comandasCancelar` das 23:58 (300 s) rodam inteiros no código antigo.

**A página de manutenção** — a da sua 190 (a), "que deixa passar só a equipe e o próprio servidor (é dele que saem as
conferências e as rotinas)" — não existe: é a escrever e a ensaiar antes (o item 6 de "pronto"; o item 10 de "O que
falta"), e tem três requisitos:

1. **deixa passar a equipe e o próprio servidor.** Os passos 10 e 11 rodam antes de liberar, e as sondas do 10, os dois
   blocos do 12 e todo o crontab saem do servidor para `https://franquiabv.com.br`: com origem no próprio IP público dele,
   ou no `127.0.0.1` se o `/etc/hosts` aponta o domínio para lá (a leitura 12). Por endereço de origem, por exemplo com um
   `geo` do nginx — a lista da equipe e o do servidor;
2. **responde 503 a todos os outros, nunca 200.** Com um 200 (um `rewrite` para um HTML), a sonda do `/novo/` dá 200, o
   `npsContabilizar` dá 200 e a conta dos 10 minutos mostra "só 200, e mais de zero" sem rotina nenhuma ter rodado — um
   verde falso. Com 503, o que bater nela sem passar aparece como 503: a passagem está errada, e **não é motivo de volta**
   — conserte a passagem, não o código;
3. **sobrevive à troca do vhost.** Se ela mora no vhost, o vhost novo do passo 8 tem de trazê-la, ou o reload a tira e
   abre o site antes das conferências; e a cópia do passo 3, que é o vhost da volta, a traz porque é tirada com ela já
   ligada. O passo 13 tira só ela.

Parar o PHP não serve aqui: sem PHP, os passos 10 a 12 não rodam, e religá-lo para rodá-los abre o site para todos antes do
teste à mão. O procedimento da etapa 2 tem a mesma lacuna da passagem, e a do `pgrep` (ver "O que a etapa 2 herda").

### 3. O backup

**O procedimento não tem passo de backup**: ele cita "o dump de antes da janela" (no "Vendas caixa" do passo 3), que na
etapa 1 não existe. **A sua 191 (a) pôs o passo 0 "com o sistema parado, depois do backup"**, e a sua 202 (b) pôs a cópia
das senhas anulada "na janela, depois do backup" — *"O backup da janela a guarda"*. A sua 197 descreveu o que existe:

> *"O Backup já é feito por rotina, de hora em hora um dump local (24 horas de rotação), outros três vps usam a replicação
> do mysql em tempo real. além do backup diário da Hostringer"*

**Decidido: a sua 204 (a)**, de 15/10/2026 — *"O dump da rotina, disparado à mão na janela: com o sistema parado e antes
das migrações; a rotina de hora em hora parada junto com o cron no começo da janela, e religada com ele no fim e na volta;
o dump copiado para fora da rotação (fora do servidor) e guardado até você declarar o fim da volta; e a restauração dele
ensaiada antes, numa máquina que não é a produção (o roteiro dela, com as três réplicas, faz parte do ensaio)."* Nenhum
dos três de hoje é, sozinho, esse backup:

- **o dump de hora em hora** — o `dumpv3.sh`, que o **D33** já nomeia — é tirado com o sistema no ar: o último antes da
  parada do passo 2 deixa fora o que se gravou depois dele; e **a rotação de 24 horas o apaga antes do fim da volta**, que
  pela sua 198 (a) dura até o dono declarar, no mínimo 48 horas. **E ele não pode disparar durante o passo 4:** um
  `mysqldump --single-transaction` segura cada tabela que já leu até terminar, e o `ALTER` que copia `vendas` espera por
  ele — ou, com o `ALTER` antes, o dump falha com "Table definition has changed". O sinal do passo 2 que olha o usuário
  da aplicação não o vê: por isso a rotina para no passo 1, com o cron, e volta no passo 12 e na volta (a 204 (a)); e o
  que já disparou antes do passo 1, o passo 2 espera terminar (o `pgrep` e o usuário do dump, em "1 e 2");
- **as três réplicas em tempo real** recebem o passo 0, o Líder e a cópia das senhas anulada (o passo 6) no mesmo
  instante: são uma cópia do agora, e não do antes — servem ao modo degradado de contingência (o **D26**), e não à volta.
  O atraso e o estado delas se conferem nos passos 4 e 13 (a leitura 21);
- **o backup diário da Hostinger:** quando roda, o que guarda (o banco, o disco, as pastas de upload, que não estão no git
  e que o **D33** dá ao `sistemacompletofbv.sh`, diário) e por quanto tempo, não se sabe daqui.

**O passo, nesta ordem** — escrito, e não ensaiado:

1. **o banco: o dump da rotina, disparado à mão**, com a manutenção no ar, o `SHOW PROCESSLIST` do passo 2 sem o usuário
   da aplicação **e sem o dump de hora em hora em curso** (o mesmo passo 2), e **antes** do laço do passo 0. O comando é o da rotina, como ela roda — o mesmo usuário, as mesmas
   opções —, e o passo se adapta a ele, sem mudar a rotina (a 204 (a)). **Ele só se escreve com os quatro fatos da leitura
   14** — se uma restauração já foi testada; o minuto, o usuário e o comando do `dumpv3.sh`; se ele leva os eventos e a
   posição do log binário; e onde ele grava —, que não vieram com a resposta, e são do dono. **O que a rotina não levar, o
   passo acrescenta à parte:** a posição do log binário, anotada (com o sistema parado ela não anda; `SHOW BINARY LOG
   STATUS` no 8.4, `SHOW MASTER STATUS` no 8.0 — a leitura 1); e os eventos — o `mysqldump` sem `--events` os deixa de
   fora, como o próprio `scripts/ensaiar-corte.sh` avisa, e um banco restaurado sem eles perde o `limpar_push_app_24h` que
   a volta religa —, com o `SHOW CREATE EVENT` dos dois do **D26** guardado ao lado do dump. Conferido: inteiro (o fim do
   arquivo, e o tamanho contra o da última rodada **inteira** de hora em hora — a que tem o fim do arquivo, e não uma que
   a espera do passo 2 tenha visto pela metade). Antes de disparar, o `pgrep` do passo 2 de novo vazio: a rotina parada
   no passo 1 não dispara, e o que já tinha disparado terminou. A base do teste tem cerca de 2,3 GB; o tamanho e o tempo
   na produção, só o ensaio da restauração dá;
2. **o dump copiado para fora da rotação e fora do servidor** (a 204 (a)), antes do laço do passo 0, e conferido lá (o
   tamanho, ou o `sha256sum` dos dois lados): a rotação de 24 horas não o alcança, e um disco perdido não o leva junto.
   Guardado **até o dono declarar o fim da volta** (a sua 198 (a)); o destino é do dono (o item 8 de "O que falta"). No
   servidor, ele fica só até a cópia de fora estar conferida (a leitura 13 conta o espaço);
3. **a restauração, ensaiada antes da janela**, numa máquina que não é a produção, com um dump da rotina — e **o roteiro
   dela num primário com três réplicas é parte do ensaio** (a 204 (a)): o dump inteiro atravessa a replicação, ou as
   réplicas se refazem dele, e o `GTID_PURGED` que o `mysqldump` escreve, se o servidor usa GTID (a medir), pode brigar
   com ele. **O roteiro não existe**: é escrita do repositório, que pede as leituras 1 e 21 na mão (se o servidor usa GTID,
   a versão de cada réplica e qual é a contingência), e o **D498** o acompanha, junto com o passo 3 escrito com o comando
   (o item 8 de "O que falta"); o ensaio é do dono. Nenhum ensaio da etapa 1 no BVIP o cobre: lá não há réplica ("O
   ensaio da etapa 1").

**Restaurar é o último recurso:** perde o que se gravou depois do dump. E o banco restaurado traz de volta a cópia antiga
das senhas que o passo 6 anulou: **depois de uma restauração, a linha do passo 6 roda de novo**, antes de abrir — é a
defesa no banco da sua 202 (b). **E o dump guarda essa cópia** — como o de hora em hora e o diário da Hostinger, por mais
tempo: é da conta da LGPD, adiada pelo dono, e nada se decide aqui.

**E as duas outras coisas deste passo**, que não dependem da 204:

- **o código antigo:** a própria troca do passo 7 o guarda — a árvore de hoje muda de nome e fica inteira, com as
  credenciais escritas nela;
- **o vhost:** uma cópia depois do passo 2 — com a manutenção já ligada, se ela mora no vhost — e antes de toda outra
  edição (o `bloquear-migrar-senhas.sh` guarda a dele sozinho). É o vhost da volta, e **não tem o bloqueio do
  `migrar-senhas`**: o passo 6 do dia 0 está pendente (`dia0-remediacao-producao.md`), e quem o repõe é a volta.

**A janela não começa sem este backup** — o passo escrito com o comando da rotina, o destino de fora e a restauração
ensaiada (o item 10 de "pronto").

### 4. O passo 0

**Antes do laço, a memória do MySQL — a sua 205 (a)**, *"Os dois na janela: com o sistema parado — a memória antes das
migrações, se a leitura der a de fábrica, no valor que a memória da máquina permitir (as cópias grandes das migrações
também rodam com ela)"*. Só se a leitura 4 deu a de fábrica (128 MB); o valor sai da memória da máquina e do que mais roda
nela (a mesma leitura), e é escrito antes da janela (o item 14 de "O que falta"). O MySQL 8 muda o `innodb_buffer_pool_size`
sem reiniciar, em blocos, e o `SET PERSIST` o guarda para o próximo início; se o servidor de lá o fixa num arquivo de
configuração, é a medir com a leitura 4 qual dos dois vale — e um reinício, se for o caminho, só com a manutenção no ar e
as três réplicas conferidas depois. Confere: o `SELECT @@innodb_buffer_pool_size` da leitura 4, de novo, com o valor
novo. A volta não o desfaz (a 205 (a)): o código antigo roda com ele. **Não ensaiado:** o BVIP já roda com a memória
ajustada (o `docker/mysql/teste.cnf`, o **D493**).

O laço é o de "O que hoje bloqueia" do `procedimento-de-corte.md`, logo depois de "Na ordem da tabela:", e **não está
repetido aqui de propósito**: o portão do passo 0 (`scripts/conferir-passo-zero-do-corte.php`) confere aquele laço contra
as linhas `| 0 |` e contra toda migração do repositório; uma cópia aqui ficaria sem portão. A do Líder fica fora do laço,
como lá (o passo 6).

**O motivo muda.** Lá, cada linha diz que a migração tem de entrar "antes do passo 1", senão some do `mysql.sql` refeito;
aqui o motivo é que **o código novo as lê**:

- **seis são condição de subir:** sem a `pontuacao-validade-unidade` e a `unidade-hash-anterior`, toda leitura de unidade
  falha — quase todo o ERP; sem a `venda-autorizacao-fiado`, toda leitura de venda; sem a `pontuacao-resgate-autorizado`,
  o histórico de pontos; sem a `usuarios-horarios`, salvar usuário ou horário dá 500; sem a `clientes-sessoes`, o pedido
  de código do `/api/v4`;
- **quatro degradam caladas**, pelo `ColunaNoBanco` (**D473**): a dos cupons, a do "Não" do pacote, a das saídas e a das
  alíquotas voltam ao comportamento de antes sem guardar o dado novo;
- a do índice de `id_pai` é só desempenho, e a dos aparelhos deixa a rota do aparelho em 503.

**O índice da grade da agenda entra também** — a sua 126 (a), *"Pelo passo 0, como o do Desmarcar (a sua 50)"*, e a sua
196 (b), *"O índice entra já na etapa 1: como mais uma migração do passo 0"*. É o do **D189**, e ele **amplia** em vez de
acrescentar: o `FK_AGENDAS_colaborador` em `agendas (colaborador)` trocado, **com o mesmo nome**, pelo de `(colaborador, data)` —
`colaborador` é prefixo dele e por isso continua servindo à chave estrangeira; o `DROP` e o `ADD` num `ALTER` só,
`INPLACE`, `LOCK=NONE`, como o das vendas por cliente —, com que a grade de um dia passou de 17,5 s a 0,72 ms na cópia. Só
desempenho, como o de `id_pai`: nenhum código depende dele, e o código antigo não o sente. **A migração existe desde
07/11/2026** (a fatia 114, ESQUEMA-QUE-SO-ACRESCENTA; o item 2 de "O que falta fazer antes da janela"):
`scripts/migracao-agendas-indice-colaborador-data.sql`, com a linha `| 0 | **Aplicar**`, o laço, o `mysql.sql` e o laço do
ensaio geral juntos (o `conferir-passo-zero-do-corte.php` saiu 0). O nome é o mesmo, e não o `IX_AGENDAS_COLABORADOR_DATA`
que este parágrafo dizia: é o que as duas cópias têm desde 02/10/2026 e o que o gerador do PostgreSQL amplia. Idempotente:
nas cópias, ela diz que já está ampliado e não muda nada; com o índice noutra forma, para com saída 1 sem mudar nada.

**E duas que só acrescentam, da mesma fatia** (a sua 83 (a)): o índice das saídas do estoque pela data
(`migracao-produtos-estoque-indice-data.sql`, o **D630**, só desempenho, `INPLACE`) e a coluna do envio do NPS
(`migracao-nps-envio.sql`, o **D648**, `INSTANT`). **A do envio é condição de subir**, como as seis de cima: a entidade da
pesquisa mapeia a coluna, e sem ela toda leitura e gravação de `nps` falha.

**O índice das vendas por cliente entra também** — a sua 294 (a), de 04/11/2026, *"Aceitar a espera, com a conta exata"*: o
índice das vendas por cliente no passo 0 da etapa 1, e não "para a migração final", como o **D308** dizia (a mesma mudança que a
sua 196 (b) fez com o da grade). É o `FK_VENDAS_cliente` em `vendas (cliente)` trocado por um maior, **com o mesmo nome**, em
`vendas (cliente, status, data_criacao)` — um `ALTER TABLE vendas DROP INDEX FK_VENDAS_cliente, ADD INDEX FK_VENDAS_cliente
(cliente, status, data_criacao), ALGORITHM=INPLACE, LOCK=NONE`, num comando só, como o D308 o ensaiou na cópia: 18 s nas
2.269.253 vendas, e a chave estrangeira `FK_VENDAS_cliente` (para `clientes`) segue servida pelo prefixo. Não apaga linha; a volta
não o desfaz, e o código antigo não o sente. **Por que nesta etapa:** a Carteira de clientes (a CA1 do
`docs/f3-relatorios/spec-carteira.md`) roda sobre este MySQL, e sem o índice a conta dela leva 6,8 a 6,9 s, em vez de 1,7 a 1,8 s
com o buffer pool de fábrica (o **D697**). **O que existe hoje, conferido só no catálogo em 04/11/2026** (o rascunho da sessão,
`p76u-01-permissoes-indice-*`): nas duas cópias locais o índice já é o de três colunas desde 29/09/2026 (no PostgreSQL,
`vendas_fk_vendas_cliente (inquilino_id, cliente, status, data_criacao)`, que o `postgres/schema.sql` já gera); o `mysql.sql` do
repositório, o retrato da produção de hoje, tem o de uma coluna, e é esse que a produção tem pelo D308 (a leitura do catálogo de lá é
a mesma do resto do passo 0: a conferência de estrutura de baixo). **A migração existe desde 04/11/2026** (a fatia 76b, a CA1 da
Carteira; o item 15 de "O que falta fazer antes da janela"): `scripts/migracao-vendas-indice-cliente-status-data.sql`, com a linha
`| 0 | **Aplicar**`, o laço, o `mysql.sql` e o laço do ensaio geral juntos (o `conferir-passo-zero-do-corte.php` saiu 0). Idempotente:
com o índice já ampliado (as cópias), ela diz que já está ampliado e não muda nada; com o índice noutra forma, para com saída 1 sem
mudar nada. Ensaiada num banco descartável com a `vendas` da cópia e o índice de uma coluna: 2,3 s (a tabela recém-carregada).

**O tempo não está medido.** Três copiam a tabela — `vendas` (2,2 milhões de linhas), `pontuacoes_historicos` (1,18
milhão) e `cupons_clientes` (100 mil; 1 s no servidor da cópia) —; os ensaios do passo 0 foram em bancos descartáveis
montados do dump, sem esse volume, e na cópia só o dos cupons tem o tempo escrito. O índice de `id_pai` leva de 11 a 20 s
(**D189**), o da grade, criar, também de 11 a 20 s nos 4 milhões de linhas (a medida do D189; o remover do índice antigo
não foi medido), o das vendas por cliente, 18 s na cópia (o **D308**, `INPLACE`, a troca inteira), e as duas `INSTANT`, 2 s
cada na cópia. Com a aplicação parada, os três que copiam não seguram ninguém:
são tempo de janela, e cada um pede, livre no disco, o tamanho da tabela que copia (a leitura 13). As réplicas
reexecutam os `ALTER`, e o atraso delas é a medir (abaixo). **Dentro da janela, com o sistema
parado e depois do backup: a sua 191 (a).** O tempo das duas cópias grandes, só o ensaio da sua 199 (a) dá, num banco
descartável do BVIP com as duas tabelas sem as duas colunas ("O ensaio da etapa 1").

**A conferência que falta, só de leitura:** depois do laço, a estrutura do banco de produção (sem dados) contra o
`mysql.sql` de hoje. A única diferença esperada é a `unidades.iframe_bi` — foi o que o ensaio de 13/10/2026 deu num banco
descartável montado do dump (1.520 linhas do `information_schema`). Na produção, nunca foi feita.

**As três réplicas, depois do laço e de novo antes de liberar** (o passo 13; a leitura 21): `SHOW REPLICA STATUS` em
cada uma, só leitura — os dois fios em `Yes`, nenhum `Last_SQL_Error`, e o atraso (`Seconds_Behind_Source`) anotado.
Atraso é o esperado: cada uma reexecuta as cópias grandes depois de o primário terminar, uma de cada vez, e num nó fraco
ele entra na manhã. **Uma réplica parada não se conserta voltando** — a volta não desfaz o passo 0 —: anote o erro; o
conserto é na réplica, do dono, e até ele a contingência de hoje e a camada de cópia que a sua 197 conta ficam sem ela.
Nenhum ensaio mede isto: no BVIP não há réplica.

### 5. A semente da vigência

A tabela `usuarios_horarios` nasce vazia no passo 4, e o código novo grava nela a cada vez que salva um usuário ou um
horário. A semente dá a cada usuário uma linha com o horário de hoje, valendo desde a última edição dele, e **só põe linha
em quem não tem nenhuma**: por isso ela vem **antes** de o código novo aceitar escrita. Quem tiver a primeira linha
gravada pelo código novo fica sem a semente, e a ocupação dele antes daquele dia cai na régua antiga do Dash (a decisão B6
do dono). O conferidor lê o banco do `local.php`, e por isso roda no passo 10, com o código novo no lugar.

### 6. O Líder, e a cópia antiga das senhas

Dois dados, os dois depois do backup e antes de o código novo aceitar escrita.

**O Líder — a sua 192 (a), "Só o Líder entra na etapa 1: na janela, com a linha que o desfaz escrita e ensaiada para a
volta".** A migração é **dado**: o grupo 8, Líder, ganha a visão de gerente (**D124**). A sua 11 (b), de 06/10/2026, a pôs
"na janela, no passo 3", porque assim "o código antigo nunca a vê" — e nesta etapa essa condição se cumpre aqui, na troca
do código. Até ela rodar, quem entra como Líder recebe os PDFs vazios, como no teste.

- **Confira que mudou uma linha.** O `UPDATE` procura o nome `Lider`: gravado de outro jeito, ele muda zero linhas, sem
  erro. O `SELECT` do cabeçalho mostra o nome antes — e o `gerente`, que tem de estar em 0 (o esperado do cabeçalho, `8 |
  Lider | 0 | 0 | 1`). **Anote o que ele mostrou:** é o que a volta repõe.
- **A volta tem de desfazê-la**: com o código antigo, o `gerente` autoriza fiado sem olhar a unidade, devolve crédito com a
  senha de um gerente de qualquer unidade e cancela venda (a linha do Líder no passo 0 do procedimento). A linha que a
  desfaz, nas mesmas tabela, colunas e condição da migração — e só se o `SELECT` de antes deu `gerente` em 0:

  ```sql
  -- no franquia_producao, na volta, com a manutencao no ar e antes de liberar (o item 4 da volta)
  UPDATE grupos SET gerente = 0 WHERE id = 8 AND nome = 'Lider';
  SELECT id, nome, gerente, caixa, colaborador FROM grupos WHERE id = 8;   -- esperado: 8 | Lider | 0 | 0 | 1
  ```

  **Escrita aqui, e não ensaiada**: o ensaio da sua 199 (a) a roda, com a migração antes, e confere as duas linhas (o item
  7 de "O que falta fazer antes da janela").
- A linha continua no passo 3 do procedimento, e na etapa 2 roda de novo sem mudar nada (é idempotente).

**A cópia antiga das senhas — a sua 202 (b), "na janela, depois do backup, a cópia antiga das senhas apagada do banco".**
É a `usuarios.senha_php83`: o código antigo a grava a cada troca de senha, cifrada de um jeito reversível com a chave no
código (o `setSenhaPhp83`, chamado pelo `criptografarSenha` da entidade `Usuario` do `eaf1fca`; o mesmo esquema da coluna
do **D78**), e só a lê na `/autenticacao/migrar-senhas` (**D2**), que escolhe `usu.status = :status AND usu.senhaPhp83 IS
NOT NULL` e grava em cada um `setSenha(getSenhaPhp83())` (a `migrarSenhasAction` do `eaf1fca`). O código novo não a mapeia
nem a grava (**D7**, **D79**), e a coluna é anulável no MySQL de produção (`DEFAULT NULL`, no `mysql.sql` de hoje e no do
dump de 16/08, `3b1f05c`). As duas cópias locais e a base do teste já vivem com ela nula — o
`scripts/anonimizar.sql.modelo` e o `scripts/endurecer-base-teste.sql` a anulam —, e o código novo roda nelas. Anulada, a
rota não acha ninguém para reescrever, mesmo se o bloqueio do nginx falhar numa volta:

```sql
-- no franquia_producao, depois do passo 3 (o backup a guarda) -- so contagens, nenhum valor impresso
SELECT COUNT(*) AS com_a_copia FROM usuarios WHERE senha_php83 IS NOT NULL;
UPDATE usuarios SET senha_php83 = NULL WHERE senha_php83 IS NOT NULL;
SELECT COUNT(*) AS com_a_copia FROM usuarios WHERE senha_php83 IS NOT NULL;   -- tem de dar 0
```

- **NULL, e nunca o vazio.** A rota filtra só o `IS NOT NULL`, e o `getSenhaPhp83` do código antigo devolve uma senha
  sorteada de 30 caracteres quando a coluna está vazia: com `''`, ela acharia todos os ativos e daria a cada um uma senha
  que ninguém sabe.
- **Não volta, e a volta não precisa dela:** o código antigo só a lê na rota que o nginx nega (o passo 8 e o item 2 da
  volta). O que ela guardava fica no backup do passo 3 — e no dump de hora em hora e no diário da Hostinger, por mais tempo:
  é da conta da LGPD, adiada pelo dono, e nada se decide aqui. As três réplicas recebem a linha junto.
- **Numa volta, ela volta a encher** para quem trocar a senha no código antigo (o `criptografarSenha` grava as duas): a
  rota continua negada no nginx, nas duas formas.
- **A outra cópia reversível fica: a `usuarios.salt`, que é o D78.** A 202 (b) cita o D78, e a cópia que ela descreve —
  "o código antigo só a usa nessa rota", "a migração para o PostgreSQL já a deixa para trás" — é a `senha_php83` (o
  **D7**; o gerador a tira, o `REMOVER` de `scripts/gerar-ddl-postgres.php`). A `salt` guarda a mesma senha, com a mesma
  chave, e este passo não a toca: o login do código antigo compara com ela (o `getSalt()`), e o código novo ainda a grava
  e a lê (o `criptografarSenha` e o `getSalt` da entidade `Usuario`, para o login por cookie e o "entrar como"). **Depois
  da janela, toda senha segue reversível no banco, nas réplicas e nos dumps, pela `salt`**: é o **D78**, que espera a
  medida da produção (`scripts/medir-senhas-producao.sql`, a leitura 17), e a LGPD, adiada pelo dono; nada se decide aqui.
- **Escrita aqui, e não ensaiada** (a sua 199 (a); o item 6 de "O que falta"): a base do teste já a tem nula, e lá a linha
  muda zero linhas — o ensaio a roda num banco descartável com valores inventados, e confere as duas contagens e que o login
  do código antigo segue entrando.

### 7. Trocar o código

**Não por cima.** Desde a importação saíram 37 arquivos do código, entre eles dois PHP que executavam no webroot sem
login (os demos do jcrop, **D188**) e o `OneSignal.php` com chaves escritas; copiar a release por cima da árvore de hoje
os deixaria no disco. A release vai para um diretório novo, ao lado, e a troca é de nome — **depois** de passar para a
árvore nova o que ela não traz, nesta ordem: primeiro a tabela e o bloco de baixo, com `public_html` ainda sendo a árvore
de hoje e `public_html.novo` a release; só então a troca (o último bloco). Na ordem inversa, o `cp -aT` não acha mais a
`public_html.novo`, o laço só diz "nao existe la", e o código novo entra no ar sem as pastas de upload.

**As campanhas na fila, antes da troca — só leitura (26/10/2026, a fatia 92b; o D609, a sua 262 (b)).** O código novo
**não manda** a campanha `agendado` que passou mais de 3 horas do começo do prazo dela (a hora marcada, entre 9h e 19h50, a
última rodada do dia; as 9h seguintes, se foi marcada antes das 9h ou depois das 19h50), salvo a que esperou a vez com a rotina mandando outras sem parar desde
então (`RegraDaCampanha::PRAZO_DA_ATRASADA_HORAS` e `saiNaRodada`; o 3 é o exemplo da sua 262 (b), e o número voltou ao
senhor na pergunta 267 — se mudar, mude o `INTERVAL` abaixo junto). O código antigo manda qualquer `agendado` vencida na
primeira rodada. Com a manutenção no ar e antes da troca, no servidor:

```sql
SELECT tipo, status, COUNT(*) AS campanhas,
       SUM(data > NOW()) AS com_a_hora_por_vir,
       SUM(data <= NOW() - INTERVAL 3 HOUR) AS passaram_3h_da_hora
  FROM notificacoes
 WHERE status IN ('agendado', 'aguardando', 'enviando')
 GROUP BY tipo, status;
```

Ela diz quantas campanhas o código novo vai mandar a partir das 09:00, quantas vai segurar — o `passaram_3h_da_hora` é um
teto: a marcada fora do horário, ou depois das 19h50, conta das 9h — e quantas são de push (a sua 254 (c): a de push agendada antes da troca sai
pelo código novo como sairia pelo antigo, se não passou do prazo; a que passou fica "Atrasada", só com o cancelar). Com o
número na mão, o senhor decide **antes das 09:00** se alguma se remarca ou se cancela, pela tela antiga. Vale mais se a
rotina estiver parada hoje na produção (a (b) do **D499**, a leitura 22): aí a fila pode ter muitas `agendado` vencidas. A
seção 8 do `scripts/medir-texto-das-campanhas.sql` é a mesma conta, rodada antes, e não substitui esta. Medido no
`franquiabv_dev` (26/10/2026, numa sessão só de leitura): a consulta roda, e a fila da cópia está vazia.

**Pelo roteiro** (o item 9 de "O que falta"): com a manutenção no ar, o cron parado e a consulta das campanhas feita,
`CRONTAB_GUARDADO=/root/crontab-antes-do-corte.txt USUARIO_DB_NO_AR=<o usuário do banco do código antigo> MANUTENCAO_ARQUIVO=<...> FRONT_RAIZ=<...> POOL_USUARIO=<...> POOL_GRUPO=<...> ENTREGA_MIN=<...> VOLTA_MIN=<...> HORA_LIMITE=05:00 bash /root/publicar-producao.sh trocar`
faz a tabela e os dois blocos abaixo, na mesma ordem e com os mesmos nomes — a release em `public_html.novo`, a árvore de
hoje em `public_html.antigo-<data>` —, e põe a pasta da tela nova do passo 8 no lugar; a única diferença é o avatar da
release, que vai para `/var/www/franquiabv.com.br/.publicar-producao/avatar-da-release-<hora>`, e não para o `/root`. Ele
para antes do recarregar: o resto do 7 (os `.sql` de depois da troca, abaixo) e do 8 segue à mão, pelo caminho da árvore no
lugar. Os blocos abaixo são o que ele faz, e o que se faz sem ele.

Antes da troca, na árvore nova:

| o quê | por quê | como |
|---|---|---|
| o `local.php` preparado | a árvore de hoje não tem um (o dia 0), e o código novo não sobe sem ele | o arquivo do item 4 de "pronto", que chegou no passo 1 direto no `config/autoload/` da release (a sua 201 (a)), com `chown root:<o grupo do pool>` e `chmod 640`, como no deploy do teste (`subir_e_ajustar`). O dia 0 só faz o `chmod`: feito como root, sem o `chown`, o arquivo fica `root:root` e o php-fpm não o lê |
| o `public/app.json` da produção | as telas de administração o reescrevem lá, e o do repositório é o de julho (**D36**) | cópia |
| o `data/` de lá (o cache dos relatórios em `data/cache/relatorios`, e os logs) | a tela de Atendimentos lê o cache que o cron grava, e sem ele mostra o vazio até a noite (a da Evasão também lia, até sair do ERP em 04/11/2026, a CA3 da Carteira; os arquivos que a rotina dela gravou em `data/cache/relatorios/evasao/` vêm junto na cópia, e ninguém mais os lê, pela sua 290 (b)) | `cp -aT` (abaixo): o `-a` guarda o dono e o modo, e a árvore de hoje fica com o dela, para a volta. Uma cópia de root sem o `-a` deixa `data/` e o que ela criar com dono root, e o php-fpm não grava |
| os proxies do Doctrine | gerados e fora do git; os de antes dão 500 na primeira página (já aconteceu, em 24/08/2026) — e o `cp -aT` do `data/` traz os de lá | o `rm -rf` do `data/DoctrineORMModule/Proxy/*` da árvore nova, **depois** do `cp -aT`, com o caminho inteiro (abaixo) |
| as pastas de upload de `public/` — `arquivos`, `contratos`, `banners`, `assistentes`, `vip-class`, `visagismo`, `logs`, se existir lá, e `layout/img/avatar` | moram no disco local e não estão no git (**D33**); cerca de 340 MB pela `operacao-atual.md` | `mv -T` para a árvore nova, no mesmo disco (abaixo); na volta, de volta, com o que se enviou no meio |
| as pastas graváveis que faltarem | o código novo grava em `data/cache`, `data/logs`, `data/logs/agenda_json`, `data/DoctrineORMModule/Proxy` e em `cache` e `tmp` de `layout/img/avatar` | criadas **depois** das mudanças de pasta, com o dono do pool, como na função `subir_e_ajustar` do deploy do teste |

**As pastas mudam com `mv -T`, e nunca com o `mv` simples.** Com o destino já existente, o `mv a b` põe `a` **dentro** de
`b`, sem erro: as fotos de perfil iriam para `avatar/avatar/` e dariam 404 no código novo, e o teste à mão do passo 11 não
olha foto. O `-T` troca o nome ou para com erro. A `layout/img/avatar` é o caso certo: ela tem três arquivos versionados
(`padrao.png`, `padrao_femi.jpg` e `padrao_masc.jpg`; o `.gitignore` os guarda), e por isso a release já a traz. E nada de
`mkdir -p` das pastas de upload antes de movê-las, como o `subir_e_ajustar` faz no teste — com elas criadas, até o `-T`
para:

```bash
cd /var/www/franquiabv.com.br
cp -aT public_html/data public_html.novo/data                     # o -a guarda dono e modo; o -T nunca cria data/data
rm -rf /var/www/franquiabv.com.br/public_html.novo/data/DoctrineORMModule/Proxy/*   # depois do cp: os proxies de la nao servem
for p in arquivos contratos banners assistentes vip-class visagismo logs; do
    if [ ! -e "public_html/public/$p" ]; then echo "public/$p nao existe la"; continue; fi
    mv -T "public_html/public/$p" "public_html.novo/public/$p" || { echo "PARE: public/$p"; break; }
done
mv -T public_html.novo/public/layout/img/avatar /root/avatar-da-release-<data>       # as tres imagens padrao da release, de lado
mv -T public_html/public/layout/img/avatar public_html.novo/public/layout/img/avatar # as fotos de la, com as padrao de la
for i in padrao.png padrao_femi.jpg padrao_masc.jpg; do cmp "/root/avatar-da-release-<data>/$i" "public_html.novo/public/layout/img/avatar/$i"; done
```

Cada `PARE` é parar e olhar, e não seguir. Se o `cmp` acusar diferença numa imagem padrão, a de lá fica (é a que o sistema
mostra hoje) e a diferença vai ao **D37**. O dono das pastas movidas fica o de lá, que é o que o php-fpm de hoje grava. **Não
ensaiado** — é parte do ensaio da etapa 1.

**Só então, a troca** — com o bloco de cima rodado e lido, sem nenhum `PARE`, e as pastas graváveis criadas:

```bash
cd /var/www/franquiabv.com.br
mv public_html public_html.antigo-<data> && mv public_html.novo public_html   # a arvore de hoje fica inteira, para a volta
```

Depois da troca, `php /var/www/franquiabv.com.br/public_html/scripts/verificar-local-php.php` (o caminho da árvore no lugar: o diretório corrente é o
`/var/www/franquiabv.com.br` da troca, onde `scripts/` não existe): sai com 1 no banco faltando e na chave que tem de estar ausente, e
o `grave` (o e-mail, o push, o Asaas, o `cron.token`, o Yungas, o WhatsApp central) não reprova — **leia a lista**.

**Depois da troca, e só depois dela: desde quando o cancelamento é gravado** (a fatia 70 da fila das telas que faltam,
PDV-CANCELAMENTO; a nota 4 dela e a 1.5 de `docs/f3-relatorios/spec-ordem.md`; a verificação da fatia 70 achou que esta etapa
não tinha o passo). A `migracao-venda-cancelamento.sql` criou as colunas no passo 4, sem a linha; a árvore nova, que grava
quando e quem cancelou, está no lugar agora; e a aplicação segue na manutenção e o cron parado até o 12 — ninguém cancela
entre a troca e a linha:

```bash
mysql franquia_producao < /var/www/franquiabv.com.br/public_html/scripts/colunas-gravadas-cancelamento.sql; echo "o mysql saiu com ${PIPESTATUS[0]}"   # termina com a linha `cancelamento`, e só ela
mysql franquia_producao < /var/www/franquiabv.com.br/public_html/scripts/colunas-gravadas-desconto-manual.sql; echo "o mysql saiu com ${PIPESTATUS[0]}"   # termina com `cancelamento` e `descontoManual`
mysql franquia_producao < /var/www/franquiabv.com.br/public_html/scripts/colunas-gravadas-valor-contado.sql; echo "o mysql saiu com ${PIPESTATUS[0]}"   # termina com `cancelamento`, `descontoManual` e `valorContado`
mysql franquia_producao < /var/www/franquiabv.com.br/public_html/scripts/colunas-gravadas-motivo-da-retirada.sql; echo "o mysql saiu com ${PIPESTATUS[0]}"   # e `motivoDaRetirada`: as quatro
mysql franquia_producao < /var/www/franquiabv.com.br/public_html/scripts/colunas-gravadas-cota-do-pacote.sql; echo "o mysql saiu com ${PIPESTATUS[0]}"   # e `cotaDoPacote`: as cinco
mysql franquia_producao < /var/www/franquiabv.com.br/public_html/scripts/colunas-gravadas-custo-do-item.sql; echo "o mysql saiu com ${PIPESTATUS[0]}"   # e `custoDoItem`: as seis
mysql franquia_producao < /var/www/franquiabv.com.br/public_html/scripts/colunas-gravadas-indicacao-do-formulario.sql; echo "o mysql saiu com ${PIPESTATUS[0]}"   # e `indicacaoDoFormulario`: as sete
mysql franquia_producao < /var/www/franquiabv.com.br/public_html/scripts/encerrar-pacotes-de-venda-cancelada.sql | tee /root/encerrar-pacotes-<data>.txt; echo "o mysql saiu com ${PIPESTATUS[0]}"   # os pacotes de venda já cancelada (D688)
mysql franquia_producao < /var/www/franquiabv.com.br/public_html/scripts/refazer-consumo-dos-clientes.sql | tee /root/refazer-consumo-<data>.txt; echo "o mysql saiu com ${PIPESTATUS[0]}"   # o consumo dos clientes (D698)
mysql franquia_producao < /var/www/franquiabv.com.br/public_html/scripts/recalcular-pontuacao-no-corte.sql | tee /root/recalcular-pontuacao-<data>.txt; echo "o mysql saiu com ${PIPESTATUS[0]}"   # a pontuacao de todos pelo historico (D284, D312)
```

**Cada roteiro pelo caminho da árvore no lugar, e a saída do `mysql` lida em cada linha** (a fatia 76o, a correção que a
76n apontou): o diretório corrente é o `/var/www/franquiabv.com.br` da troca, e ali `scripts/` não existe -- chamado pelo
caminho relativo, o redirecionamento falha, o `mysql` nem roda e a linha só diz o erro do shell. E com o `| tee`, o `$?` é o do
`tee`, que sai com 0 mesmo quando o roteiro para: o `echo` do `PIPESTATUS[0]` diz a do `mysql` (nas linhas sem `tee` ele é o
próprio `$?`, escrito do mesmo jeito para nenhuma linha ficar de fora). **Cada linha diz "o mysql saiu com 0"; outro número é
`PARE`.** O `tests/Caracterizacao/PermissoesComONomeDaTelaTest.php` cobra as duas coisas de todo roteiro deste passo.

**A segunda linha, logo depois, é a do desconto manual** (a fatia 71, PDV-DESCONTO-LANCADO-POR; a P2 do `spec-ordem.md`): a
árvore nova grava quem lançou o desconto digitado no PDV (`vendas_produtos.desconto_lancado_por`, a coluna que a
`migracao-venda-cancelamento.sql` criou no passo 4), e a linha `descontoManual` diz ao Descontos "gravado desde este instante".
O mesmo porquê da de cima: com a aplicação na manutenção, ninguém lança desconto entre a troca e a linha. Idempotente também.

**A terceira, logo depois, é a do valor contado** (a fatia 72, PDV-GAVETA-CONTADA; a pergunta 15 (c) do dono e a P1/P2 do
`spec-ordem.md`): a árvore nova faz o fechamento do caixa pedir o dinheiro contado na gaveta e o grava
(`caixas.valor_contado`, a coluna que a `migracao-caixa-valor-contado.sql` criou no passo 4), e a linha `valorContado` diz ao
relatório de Caixa "a quebra existe desde este instante". **Depois da do cancelamento, e não antes:** a quebra só sai para o
caixa fechado a partir das duas datas, porque a gaveta ao fechar devolve o dinheiro das canceladas DEPOIS do fechamento, e
isso pede que o cancelamento já grave o instante (o invariante em `ContasDoCaixa::temQuebra`). Com a aplicação na manutenção,
ninguém fecha caixa entre a troca e a linha; o caixa que estiver aberto na troca fecha depois dela, já pedindo a contagem.
Idempotente também.

**A quarta, logo depois, é a do motivo da retirada** (a fatia 73, PDV-MOTIVO-DA-RETIRADA; a pergunta 9 (a) do dono e a P4 do
`spec-ordem.md`): a árvore nova faz a saída pela tela de estoque (a antiga, a nova e a API) pedir o motivo -- perda, vencimento
ou acerto de inventário -- e a retirada de uso interno (o PDV antigo e a comanda nova) gravar `uso`
(`produtos_estoque.motivo`, a coluna que a `migracao-produtos-estoque-motivo.sql` criou no passo 4), e a linha
`motivoDaRetirada` diz à Retirada de Produtos "a perda, o vencimento e o acerto se separam desde este instante". Não depende
das outras três: a ordem é só a do passo. Com a aplicação na manutenção, ninguém lança saída de estoque entre a troca e a
linha. Idempotente também.

**A quinta e última, logo depois, é a da cota do pacote** (a fatia 74, PDV-COTA-DO-PACOTE; a pergunta 16 (b) do dono e a P5
do `spec-ordem.md`): a árvore nova faz o cadastro do pacote pedir o percentual da cota-parte de quem vendeu
(`produtos.cota_parte_pacote`, zero por padrão) e a finalização de toda sessão de pacote gravar quem vendeu e a cota dele
(`vendas_produtos.vendedor_do_pacote` e `cota_do_vendedor`, as colunas que a `migracao-cota-do-pacote.sql` criou no passo 4), e
a linha `cotaDoPacote` diz à Cota-parte "a sessão é creditada também a quem vendeu desde este instante". Não depende das outras
quatro: a ordem é só a do passo. Com a aplicação na manutenção, ninguém finaliza sessão de pacote entre a troca e a linha.
Idempotente também.

**A sexta, logo depois, é a do custo do item** (a fatia 74b, PDV-CUSTO-DO-ITEM; a resposta 224 (b) do dono, de 15/10/2026): a
árvore nova grava em cada item de produto vendido (o PDV antigo, a comanda nova e a agenda) o custo do cadastro naquele momento
(`vendas_produtos.custo_unitario`, a coluna que a `migracao-custo-do-item.sql` criou no passo 4), e a linha `custoDoItem` diz ao
Produtos / Serviços "a margem dos produtos existe desde este instante" e à Retirada "a cortesia vai pelo custo gravado". Não
depende das outras cinco: a ordem é só a do passo. Com a aplicação na manutenção, ninguém vende entre a troca e a linha.
Idempotente também.

**A sétima, logo depois, é a da indicação do formulário** (a fatia 75, REL-INDICACAO-PORTA; as respostas 264 (b) e 265 (b) do
dono, de 25/10/2026): a árvore nova grava a porta em toda linha de convite (`indicacoes.porta`, a coluna que a
`migracao-indicacao-porta.sql` criou no passo 4, junto com o passado do formulário), dá a linha também ao amigo de quem indica
inativo, sem a ligação, e a etapa 3 desse amigo marca a adesão nela; e a linha `indicacaoDoFormulario` diz ao Programa de
Indicação "a adesão pelo formulário é contada desde este instante". Não depende das outras seis: a ordem é só a do passo. Com a
aplicação na manutenção, ninguém conclui o cadastro entre a troca e a linha. Idempotente também.

**Logo depois das sete, os pacotes de venda já cancelada, encerrados uma vez** (a fatia 74g, RESPOSTAS-286-E-287; a resposta
287 (a) do dono, de 03/11/2026, o **D688**). Desde a fatia 74f (a resposta 285 (a), o **D686**) cancelar a venda encerra o pacote do
cliente que ela criou; os pacotes de vendas canceladas antes disso continuam ativos (na cópia, em 03/11/2026: 118, de 66 vendas e 39
clientes, nenhum dentro da validade). `scripts/encerrar-pacotes-de-venda-cancelada.sql` os encerra com a mesma marca do
cancelamento novo (`status` falso e `data_alteracao`, o mesmo instante em todos; nada apagado, o restante como estava), e só eles:
não toca o pacote de venda válida nem o já encerrado. **A contagem sai antes do `UPDATE`** -- quantos são, quantos ainda estavam
dentro da validade, quantas sessões restavam (nos de saldo positivo e nos ainda válidos), quantos com o saldo negativo, de quantos
clientes e de quantas vendas --, e depois dela a lista dos que vão ser encerrados, com a `data_alteracao` de antes: o `tee` guarda
as duas em `/root/encerrar-pacotes-<data>.txt`. **Leia a contagem e anote-a no relato da janela; não é ponto de parada:** a resposta
é encerrar todos ("se houver pacote ainda válido você vê o número"), e o número dos ainda válidos vai ao dono. A conferência no fim
diz 0 ativos de venda cancelada; outro número é `PARE`. **Depois da troca, e não no passo 0:** com o código antigo no lugar, a venda
cancelada entre o roteiro e a troca deixaria o pacote ativo; aqui, com a aplicação na manutenção, ninguém cancela no meio. **Na etapa
2** a carga (`migrar-dados.sh`) leva os pacotes encerrados ao PostgreSQL, e o irmão (`postgres/encerrar-pacotes-de-venda-cancelada.sql`,
no 7d de lá) conta 0 e não muda nada. **Numa volta** os pacotes ficam encerrados (o código antigo também não os oferece de volta); para
desfazê-los, a lista guardada: `UPDATE clientes_pacotes SET status = TRUE, data_alteracao = <a de antes> WHERE id = <id>`, uma por
linha. Idempotente: rodado duas vezes, a segunda conta 0 e não muda nada. Ensaiado em 03/11/2026 num banco descartável montado da
cópia, nos dois bancos, duas vezes (os números no **D688**). **Não ensaiado na janela** — é parte do ensaio da etapa 1.

**Logo depois dos pacotes, o consumo dos clientes refeito uma vez** (a sua 295 (a), de 04/11/2026, *"Consertar a conta"*; o
**D698**). A coluna `clientes.consumo` — o total das vendas finalizadas do cliente, que o Clientes antigo, as Campanhas e a Carteira
leem — só é refeita pela rotina da 01:10 para quem veio nos dois últimos dias, e nada a refazia quando uma venda era cancelada; o
código novo desta etapa já traz a rotina que refaz também quem teve venda cancelada (a CA7 da Carteira, o item 16). O roteiro do
item 16, `scripts/refazer-consumo-dos-clientes.sql` (a CA7, a fatia 76o, 04/11/2026), **conta antes** — quantos clientes mudam, quantos ficam acima e abaixo da soma, quantos com
zero e venda, quantos sem venda com valor —, **refaz** a coluna para a soma das vendas finalizadas (zero para quem não tem nenhuma)
onde ela é nula ou difere em mais de 1 centavo, em centavos inteiros — o nulo de quem nunca comprou vai a zero (a sua 303 (a), de
04/11/2026, *"Zero nos dois"*, o **D707**: quem não tem compra finalizada fica com zero, como o cadastro novo nasce; depois do corte ele
entra em "gastou até R$ X" no Clientes antigo e nas Campanhas) —, e **conta depois** (zero). O `tee` guarda as duas contagens e a lista dos que mudam, com o número de antes, em `/root/refazer-consumo-<data>.txt`.
**Leia a contagem e anote-a no relato; não é ponto de parada.** Na cópia, em 04/11/2026, só por SELECT: 9.184 clientes mudariam (os
4.707 da conta da 76o — os 4.477 ativos do D698, e o resto entre inativos e os dois sentinelas — e, pela 303 (a), os 4.477 nulos de
quem nunca comprou, 3.508 deles ativos), a consulta que os acha leva cerca de 6 s; e o roteiro inteiro, com o `UPDATE` dos 9.184,
cerca de 19 s num clone descartável da cópia (18 s na medida da 76p), e 16 s na segunda rodada, que não muda nada (abaixo). **Depois da troca, e não no passo 0:** muda dado, e o passo 0 é só esquema; e com o código antigo no lugar, a venda cancelada
entre o roteiro e a troca deixaria o número atrasado outra vez. **Numa volta ele fica**: o código antigo lê o mesmo número, e a rotina
de lá o refaz para quem vem, como sempre — mas o cancelamento feito na volta não grava o `cancelado_em`, e a rotina nova, de volta ao
código novo, não o acha: esse cliente fica atrasado até voltar, ou até o roteiro rodar de novo (idempotente). **Na etapa 2** a carga
leva o número refeito, e nenhum passo de lá muda (não há irmão em `postgres/`). **Ensaiado num banco descartável** (o
`tests/Caracterizacao/ConsumoDoClienteTest.php`, no MySQL: a contagem antes, o `UPDATE` só nos de mais de 1 centavo e nos nulos,
o nulo sem venda a zero, a conferência que mostraria um nulo deixado, o sentinela refeito, a segunda rodada com zero) **e num clone descartável da cópia** (04/11/2026, a 76o e a
76p: os números no **D698** e no **D707**); **não ensaiado na janela** — é parte do ensaio da etapa 1 (o item 11).

**Logo depois do consumo, a pontuação de todos recalculada pelo histórico** (a fatia 106, de 07/11/2026; as suas 230 (a) e
123 (a), nessa ordem; o **D312** e o **D284**). A coluna `clientes_creditos.pontuacao` é o saldo de pontos de cada cliente em
cada unidade, e sai do histórico: o `ClienteService::atualizarPontuacao` grava nela a soma das linhas `ativa` da mesma unidade. O
código antigo a deixava torta — o cancelamento tirava reais, e não os pontos (o **D284**), e os 10 pontos da indicação iam para a
coluna da unidade do cadastro de quem indicou, com o registro na unidade da compra (o **D312**) —, e ela só se acerta no próximo
recálculo de cada cliente. `scripts/recalcular-pontuacao-no-corte.sql` faz o de todos de uma vez: **primeiro a sua 230 (a)** —
a linha de saldo zerada (cliente, unidade) de cada par com pontos `ativa` e sem saldo, como o código novo a cria, para o
recálculo ter onde pôr os pontos; ninguém perde ponto, e eles ficam na unidade da compra, como a sua 92 (b) —, **depois a sua
123 (a)** — a soma das linhas `ativa` em toda linha de saldo que difere dela, a mesma conta do serviço, também nas duas linhas do
par gêmeo. Só a coluna `pontuacao` e as linhas novas: o crédito em dinheiro (`valor`, `valor_pf`), também o dos pontos de venda
cancelada que já viraram crédito, fica com o cliente (a 123, pela lógica da sua 68); nenhuma linha do histórico é tocada.
**Conta antes** — as linhas de saldo, os pares sem saldo e os pontos deles, e, das linhas que existem, quantas diferem, acima,
abaixo, nulas, e o líquido (a coluna menos o histórico) —, e imprime **as listas** dos pares sem saldo, das linhas criadas (o id)
e de cada linha que muda, com o número de antes (só ids e números): o `tee` as guarda em `/root/recalcular-pontuacao-<data>.txt`, e
é o caminho de volta (o cabeçalho do roteiro). **Leia a contagem e anote-a no relato; não é ponto de parada:** a resposta é
recalcular todos. A conferência no fim repete a contagem de antes: as linhas de saldo são as de antes mais as criadas, e todo o resto zero;
outro número é `PARE`, e a mesma linha diz quantas ficaram acima, abaixo e o líquido. Na cópia, em 07/11/2026, só por SELECT: 1 par sem
saldo, de 10 pontos (o lançamento de indicação do **D312**); no PostgreSQL da cópia, que nenhuma rotina tocou, 28 das 256.987
linhas de saldo diferem (22 acima, 6 abaixo, +5.092); no MySQL de desenvolvimento, 7.793 (quase todas linhas que expiraram
no próprio ambiente, depois do retrato). O roteiro inteiro levou cerca de 3 s num clone descartável das duas tabelas da cópia
MySQL (1,18 milhão de linhas de histórico), com 1 linha criada e 7.794 recalculadas, e a segunda rodada, também cerca de 3 s, não
mudou nada. **Depois da troca, e não no passo 0:** muda dado, e o passo 0 é só esquema (a sua 83 (a)); e com o código antigo no
lugar, a venda cancelada ou a indicação em outra unidade entre o roteiro e a troca deixaria a coluna torta outra vez. **Numa volta
ele fica:** o código antigo lê a mesma coluna e a recalcula pela mesma soma; o cancelamento antigo, na volta, volta a tirar reais,
e o roteiro, rodado de novo depois da volta ao novo, acerta (idempotente). **Na etapa 2** a carga leva o número recalculado, e o
irmão (`postgres/recalcular-pontuacao-no-corte.sql`, no 7d de lá) conta e acerta só o que o intervalo deixou. **Ensaiado** no
`tests/Caracterizacao/RecalculoDaPontuacaoNoCorteTest.php`, nos dois bancos (o indicador de outra unidade, o cancelamento antigo,
os pontos resgatados com o crédito que fica, o par gêmeo, a pontuação nula, a segunda rodada com zero, e o `atualizarPontuacao`
que, depois dele, não muda nada) e no clone descartável acima; **não ensaiado na janela** — é parte do ensaio da etapa 1 (o item
11).

**E as quatro permissões com o nome da tela** (a sua 298 (c), de 04/11/2026, *"As quatro passam ao nome da tela no dia da virada"*;
o **D701**). Com o código novo no lugar e a manutenção no ar, o roteiro do item 17 (a DE3 das Datas da equipe, 04/11/2026)

```bash
mysql --default-character-set=utf8mb4 franquia_producao < /var/www/franquiabv.com.br/public_html/scripts/renomear-permissoes-das-telas-novas.sql | tee /root/renomear-permissoes-<data>.txt; echo "o mysql saiu com ${PIPESTATUS[0]}"
```

**A saída que vale é a do `mysql`, e não a da linha:** com o `| tee`, o `$?` é o do `tee`, que sai com 0 mesmo quando o roteiro
para — por isso o `echo` do `PIPESTATUS[0]` logo depois, na mesma linha (o mesmo vale para os outros roteiros com `| tee` deste
passo). O caminho é o da árvore nova, já no lugar (o diretório corrente é o `/var/www/franquiabv.com.br` da troca). O roteiro
renomeia quatro linhas de `funcionalidades` num `UPDATE` só, cada uma pelo id **e** pelo nome antigo (os ids da cópia, lidos por
SELECT em 04/11/2026; o `UPDATE` só muda a linha em que os dois casam, e quem confere o nome é a parada de depois, abaixo):

| id | o nome de hoje | o nome novo |
|---|---|---|
| 49 | Relatórios de aniversariante | Relatórios de datas da equipe |
| 40 | Relatórios dashboard colaboradores | Relatórios do profissional parceiro |
| 15 | Relatórios de clientes | Relatórios da carteira de clientes |
| 17 | Relatórios de vendas | Relatórios da cota-parte |

e **para sozinho** em dois casos (abaixo): um nome novo já em outro id, antes do `UPDATE`, e um dos quatro ids sem o nome novo,
depois dele. **O resto o roteiro mostra, e quem confere é você:** as linhas de `privilegios` e de `privilegios_usuarios` das quatro
com a mesma contagem de antes — os privilégios ligam pelo id, e o `UPDATE` do nome não as alcança, então ninguém perde permissão —
e nenhuma linha com um dos nomes antigos (a última linha da saída, que diz 0). Nenhuma das duas é parada: o `UPDATE` não tem como
mudar a contagem, e um nome antigo repetido noutro id já derrubaria o login de hoje. A "Relatórios dashboard colaboradores - plano
carreira" não muda. A saída mostra o `ANTES` (id, nome, moldes de grupo e linhas de pessoas), o `MUDOU` (4 na primeira vez, 0 nas
seguintes), o `DEPOIS`, com as mesmas contagens, e o `DEPOIS, NENHUMA LINHA COM UM DOS NOMES ANTIGOS`, com 0. **O id que não fica
com o nome novo é `PARE`**: o id da produção não é o da cópia, ou
o nome está gravado de outro jeito — o roteiro confere dentro da transação, lista o id e sai com 1 ("Column
'PARE_um_dos_quatro_ids_nao_ficou_com_o_nome_novo' cannot be null"), e o InnoDB desfaz os quatro; leia `SELECT id, nome FROM
funcionalidades WHERE nome LIKE 'Relat%'` e ajuste o id no roteiro, nunca o nome. **O nome novo já em outro id também é `PARE`**, e
o roteiro o confere sozinho antes do `UPDATE` (zero linhas com o nome novo fora do id dele), lista a linha e sai com 1 sem mudar
nada ("Column 'PARE_o_nome_novo_ja_existe_em_outro_id' cannot be null"): dois nomes iguais em `funcionalidades` derrubam o login de todos (o `Acl` lança no segundo `addResource`, o incidente de
15/09 do comentário do `tests/Caracterizacao/fixtures.sql`), e nenhum banco o impede — o MySQL não tem índice no nome. No ensaio (o
item 11), o clone da cópia recebe as fixtures dos testes, e elas cedem também pelo nome antigo (a DE3): o nome novo só chega ao clone
por este `UPDATE`. **Por que com o código, e
nunca antes nem sem ele:** o `SessaoAcl` pergunta ao ACL pelo nome de cada funcionalidade no login de todos, e o ACL lança com um nome
que a tabela não tem — o código antigo, que pergunta pelos nomes antigos, deixaria ninguém entrar com as linhas renomeadas. O código
novo pergunta pelo nome novo e, sem ele, pelo antigo (a DE3), e por isso a ordem dentro do passo não importa; mas **a volta devolve os
quatro nomes antes de o código antigo servir** (o item 4 da volta, e a tabela "O que quebra a volta"). **Na etapa 2** a carga leva os
nomes novos ao PostgreSQL, onde o `postgres/schema.sql` declara o nome único (`funcionalidades_nome_unico`, `:1943`), e nenhum dos
quatro existe lá. **A cópia local do PostgreSQL não tem essa restrição** (só a `funcionalidades_pkey`, lida no catálogo na verificação
da 76u; a diferença entre a cópia e o `schema.sql` é anterior a esta etapa), e por isso nenhum ensaio feito dela exercita a
unicidade: quem protege contra o nome repetido, nos dois bancos, é a guarda do roteiro acima. Idempotente: rodado de novo, muda zero
linhas e a conferência passa. **Ensaiado num banco descartável** (o `scripts/ensaiar-permissoes-com-o-nome-da-tela.php`, no
`scripts/conferir-tudo.sh`: a ida e a volta duas vezes, as dez paradas, a "plano carreira" intacta, e as fixtures por cima do
catálogo antigo nos dois bancos); **não ensaiado na cópia nem num clone** — é parte do ensaio da etapa 1 (o item 11).

**Por que aqui, e não no passo 4 nem no 7d da etapa 2:** a linha diz aos relatórios "o PDV grava o instante e o gerente desde
este instante". Escrita no passo 4, antes da troca, ela diria isso com o código antigo ainda no lugar; deixada para o 7d da
etapa 2, os Cancelamentos no período, o "antes ou depois do fechamento" do Caixa e a lista do Royalties diriam "ainda não
gravado" durante as semanas entre as duas etapas, com o dado já gravado, e depois do 7d a data seria a da etapa 2 — os
cancelamentos gravados entre as duas ficariam de fora calados (a leitura começa na data). **Na etapa 2 ela não muda:** a carga
do passo 4 de lá (`migrar-dados.sh`) copia a `colunas_gravadas` do MySQL com esta linha, e o 7d de lá é idempotente — não escreve
se a linha existe, e a data continua a desta janela. **Numa volta** ela fica (a tabela "O que fica no banco", abaixo). Idempotente
aqui também: rodado duas vezes, a segunda não muda o instante. **Não ensaiado** — é parte do ensaio da etapa 1.

**Nunca o `local.php` novo na árvore antiga** — nem antes da janela (o item 4 de "pronto"), nem numa volta. O Laminas lê todo
`config/autoload/{,*.}local.php`: numa volta, a árvore de hoje volta como estava, sem ele, e o código antigo segue com as
credenciais escritas nele.

### 8. A tela nova e o nginx

- **O `dist` da build** vai para a raiz da tela nova, fora da árvore do código, com a troca da função `trocar_front` do
  deploy do teste (a pasta entra como `novo.entrando`, a anterior fica como `novo.anterior`).
- **As três `location`** do molde, no `server {}` do site, que serve o `www` e o raiz (os dois respondem 200 sem
  redirecionamento, `f1-6-agendamento.md`): a tela nova depende do cookie de sessão do ERP, que é do host (o comentário de
  `frontend/vite.config.ts`), e tem de estar no mesmo nome por onde a pessoa entrou. A `/adm/api` vai ao PHP como qualquer
  rota do ERP: nada a acrescentar para ela.
- **O vhost novo primeiro, e o bloqueio depois.** O vhost novo (o item 5 de "pronto") já traz as duas negações abaixo e a
  manutenção; em seguida, `sh /var/www/franquiabv.com.br/public_html/scripts/bloquear-migrar-senhas.sh --aplicar`, que diz
  "O bloqueio já está no arquivo" e não faz nada, ou o põe — pelo caminho da árvore no lugar, como os roteiros do 7: o
  diretório ainda é o `/var/www/franquiabv.com.br` da troca, onde `scripts/` não existe, e pelo caminho relativo o `sh`
  só diria que não abre o arquivo. Na ordem inversa, o vhost novo escrito antes apagaria o bloqueio que o roteiro acabou de pôr. **O
  "já está" do roteiro não prova a forma:** ele procura o texto `autenticacao/migrar-senhas` em qualquer linha do vhost
  (o `grep -q` de `scripts/bloquear-migrar-senhas.sh`) — a negação do `/index.php/` sozinha, ou até um comentário,
  bastam para ele não pôr a `location =`. Quem prova as duas formas é a leitura do `nginx -T`, abaixo.
- **As duas formas do `migrar-senhas`:** o código novo não tem mais a rota (**D2**), e uma volta a traz de novo — e, depois
  desta etapa, com um dano novo (a `senha_php83`, em "Um sistema só gravando"). O roteiro bloqueia só
  `location = /autenticacao/migrar-senhas`, e `/index.php/autenticacao/migrar-senhas` chega ao mesmo roteamento por fora
  dele: o repositório mediu isso para o prefixo `/index.php/` (o 9 de "O que hoje bloqueia" do procedimento; "o mesmo
  roteamento, por outro caminho", em `docker/nginx/teste-host.conf.modelo`). Por isso o vhost novo traz também a negação
  dessa forma — `location ~* ^/index\.php/autenticacao/migrar-senhas(/|$) { deny all; }`, antes de toda outra
  `location ~` do `server {}` —, e a volta a repõe. A negação do `/index.php/` inteiro, como no teste, é mais larga, e é
  do dono (abaixo). **A conferência é ler o vhost** (`nginx -T 2>/dev/null | grep -n 'migrar-senhas'`, as duas linhas):
  um `curl` na rota a dispararia no código antigo, se o bloqueio não estivesse lá. **Decidido: a sua 202 (b)** — as duas
  formas fechadas aqui e de novo na volta, conferidas lendo a configuração, **e** a defesa que não depende do nginx: a
  cópia antiga das senhas anulada no passo 6, depois do backup.
- **O `/arquivos/` fechado inteiro (o D355; 22/10/2026, a fatia ARQUIVOS-SERVIDOR).** O vhost novo traz
  `location ~* ^/arquivos/ { return 404; }` como a PRIMEIRA `location ~` do `server {}` (as expressões regulares valem na
  ordem do arquivo; o molde é o "Bloco de segurança 0" de `docker/nginx/default.conf`). **Sem caixa (`~*`), e não `^~`:**
  o `^~` compara a caixa, e `/ARQUIVOS/<nome>` passava por ele -- medido aqui, no disco do Windows que não diferencia a
  caixa (a verificação da fatia, 22/10/2026); lá o disco provavelmente diferencia, e o `~*` não depende disso. Hoje o nginx de lá serve a biblioteca da
  franqueadora (109 PDFs e planilhas, 74 MB) a quem tiver o nome, sem sessão, e desativar um arquivo não o tira do ar; o
  código desta etapa entrega o arquivo só com sessão e com o privilégio (`EntregaDoArquivo`: a
  `/adm/api/arquivos/:id/conteudo` e o "abrir"/"baixar" do ERP), lendo pelo `Armazenamento`, e não pela URL — nenhuma
  tela aponta para `/arquivos/`. **A conferência é ler o vhost** (`nginx -T 2>/dev/null | grep -n -A1 'location ~\*
  \^/arquivos/'`, o bloco e o `return 404;`, e nenhuma `location ~` antes dele), e não um `curl` com um nome de verdade,
  que ficaria no log. **O BVIP** não passa por aqui: o vhost do host de lá (`docker/nginx/teste-host.conf.modelo`)
  repassa o `location /` à pilha (`proxy_pass`), e quem serve `/arquivos/` é o nginx da pilha, que monta o
  `docker/nginx/default.conf` deste repositório -- o bloco chega lá com o código, na próxima entrega pelo
  `scripts/deploy-teste.sh`, sem mudar o molde nem a carga do deploy. Depois dessa entrega, no servidor do teste, a
  conferência é `docker compose exec -T nginx nginx -T 2>/dev/null | grep -n -A1 'location ~\* \^/arquivos/'` (o
  bloco e o `return 404;`); se não aparecer, o nginx da pilha não foi religado com o `default.conf` novo. **A volta não precisa reabrir:** o código de antes também lê o arquivo pelo `Armazenamento` (o D296) — o
  vhost de antes, que a volta repõe, o reabre sem quebrar nada, e o furo volta com ele.
- **O que não vem do vhost do teste:** a negação de `/api/unidade`, `/api/cronjobs` e `/api/marketing` (a produção as usa),
  a senha HTTP e a marca `Secure` que o proxy do teste põe nos cookies (na produção não há proxy). O que vale como molde, se
  o vhost de lá não tiver (a leitura 12): as negações de `/index.php/` e da `/ocra_service_manager_yuml`, e as duas regras
  de proteção do `docker/nginx/default.conf` (o PHP nas pastas de upload e os `.txt` e `.sql` soltos). Acrescentá-las é
  mudança no servidor, do dono.
- **Os três requisitos do php-fpm entram aqui — a sua 205 (a)**, *"o php-fpm junto com o vhost novo e o recarregar que a
  janela já faz (o pool das rotinas é um arquivo do php-fpm e uma entrada a mais no vhost). A volta não precisa desfazer
  nenhum dos dois; com o vhost de antes, as rotinas voltam ao pool de todos."* Os três do item 4 de "Preparação" do
  procedimento (o **D67**): o `request_terminate_timeout` acima de 21 minutos, **um pool separado para as rotinas** e mais
  processos, nunca sozinhos. O pool das rotinas é um arquivo novo do php-fpm, com o socket dele; o vhost novo ganha a
  entrada que manda as rotas das rotinas a esse socket — `/api/cronjobs`, `/api/marketing` e
  `/api/cupons/cuponsValidarVencimento`, as mesmas que o `docker/nginx/teste-host.conf.modelo` nega no teste, em qualquer
  grafia; **não** o `/api/cupons` inteiro, que tem rota do aplicativo (`/api/cupons/selecionar`) —, e o resto segue no pool
  do site. **O teto do pool do site, até o dono declarar o fim da volta, é o das rotinas** (o requisito 1, acima de 21
  minutos; decidido em 15/10/2026, a 228 (a), o D497), e não
  o teto curto que o requisito 2 do D67 dá ao do aplicativo: a 205 (a) diz que a volta não desfaz o php-fpm e que, "com o
  vhost de antes, as rotinas voltam ao pool de todos" — o pool do site. Com o teto curto nele, toda rotina que passasse
  dele, depois de uma volta, seria morta pelo processo mestre, toda noite, e o crontab de antes cala o erro (o
  `> /dev/null 2>&1`): a mais longa das que ficaram não foi medida (o item 4 de "O que falta medir"), e o
  `relatoriosRoyaltiesAtualizar` tem 600 s no crontab. Das três saídas que foram ao lote (o D497) — o teto curto só
  depois da declaração do fim da volta, ou a volta desfazendo o teto, ou a volta mantendo o pool das rotinas —, **o dono
  escolheu a primeira, a 228 (a)**: *"até lá, os dois grupos com o tempo das rotinas; depois da declaração, o tempo curto
  entra numa entrega à noite, como as outras. A volta fica como a sua 205 (a), sem nada a desfazer."* O preço, que a
  opção dizia: nesses dias (no mínimo 48 horas), um pedido do site travado pode prender um processo do site por até meia
  hora. O teto curto se escreve junto, e entra pela regra das entregas ("Depois da janela"). Os dois
  arquivos se escrevem antes da janela, com os pools e os valores de hoje na mão (a leitura 7; o
  item 14 de "O que falta"), e entram aqui, junto com o vhost; o recarregar do passo 9 os põe no ar. **Confere:** o
  `nginx -t` do vhost com a entrada nova, e, depois do 9, o pool das rotinas de pé (a lista de processos do php-fpm, ou a
  página de status, se houver). **Não ensaiado:** no BVIP não roda cron, e o pool das rotinas não tem quem o chame.

### 9 e 10. Recarregar o PHP-FPM, e conferir sem escrever

Recarregar é obrigatório com o `opcache.validate_timestamps` desligado, e recomendável em qualquer caso: sem ele, o
OPcache pode servir arquivos das duas árvores ao mesmo tempo (a leitura 7). O recarregar não espera o pedido em curso (o
`process_control_timeout`): é por isso que o passo 2 esperou não haver nenhum. Depois, nada aqui escreve — e tudo roda **do
servidor, por dentro da manutenção**, que tem de deixá-lo passar (o requisito 1 de "1 e 2"):

```bash
cd /var/www/franquiabv.com.br/public_html
curl -s -o /dev/null -w "%{http_code}\n" https://franquiabv.com.br/api/cronjobs/npsContabilizar   # 401: o codigo novo (o antigo, 200; a manutencao, 503)
php scripts/conferir-vigencia-do-horario.php --plataforma=mysql    # tem de sair com 0
php scripts/ensaiar-vigencia-do-horario.php --plataforma=mysql     # tem de sair com 0; desfaz o que grava
curl -s https://franquiabv.com.br/novo/ | grep -c 'src="/novo/assets/'                          # 1 ou mais: a build, e nao a manutencao
grep -rn "migrar-senhas" module/ config/                          # so o comentario do ISVenda -- NUNCA um curl nessa rota
```

**Um 503 aqui é a manutenção**, que não deixou o servidor passar: conserte a passagem e repita, sem volta. O `/novo/` se
confere pelo conteúdo, e não pelo código: uma página de manutenção com 200 passaria num `-w "%{http_code}"`. O primeiro é o
do passo 7 do dia 0 e o do começo do passo 9 do procedimento: sem token, a rotina recusa, e não roda. Os
dois do meio são os do 7b do procedimento com `--plataforma=mysql` (os dois roteiros aceitam as duas plataformas); rodados
com o `local.php` apontando para outro lugar, saem com 2 e dizem para onde ele aponta. O último é a verificação segura do
dia 0: um `curl` na rota de migração de senhas a dispararia.

### 11. O teste à mão

**Quem — a sua 190 (a):** "Testa você, ou quem você indicar, com um usuário de verdade de cada papel (administrador,
gerente, caixa, colaborador e Líder)", pela página de manutenção, de um dos endereços da equipe. **O quê:**

- os cinco testes de fumaça do passo 4 do dia 0 — o login; o TXT do relatório de clientes, que baixa e não aparece em
  `public/`; uma notificação push, **ao aparelho de quem testa**, e não a um cliente; o "Esqueci minha senha"; uma tela de
  visagismo;
- entrar com cada papel, e entrar de novo digitando o login com a primeira letra maiúscula;
- abrir `/novo/`, a agenda do dia e a comanda; lançar uma venda com desconto e fechá-la, pela tela nova e pelo PDV antigo —
  **a venda de teste** (abaixo);
- abrir o relatório de comissões e o dashboard;
- uma rota do aplicativo, sem sessão: reabertas (a sua 189 (b)), ela responde ao aplicativo como hoje — um 401 aqui é
  o código de reabrir que não entrou, e é volta;
- **nada das duas rotinas do Asaas.** A primeira conversa do código novo com o Asaas é a das 12:30 depois da janela (o
  "Depois", abaixo).

**A venda de teste é venda de verdade, no banco de produção** — é a primeira vez aqui; o procedimento da etapa 2 tem o
mesmo teste, sem dizer o que fazer com ela. Fechada (a situação 1), ela entra no `npsNotificacoes`, que pega toda venda
fechada sem pesquisa, sem limite de data (`VendaRepository::selecionarVendasComNotificacaoNpsPendente`): de 5 em 5 minutos
depois do passo 12, a pesquisa sai pelo WhatsApp ao cliente da venda. E o retrato das 05:25 a soma no royalty da unidade,
faturado no dia 16, com o caixa, a comissão e os pontos. **Decidido: a sua 203 (a)** — *"A venda no cadastro de quem
testa: com o telefone dele, numa unidade combinada, e cancelada pela tela antes de as rotinas voltarem, com o motivo
'teste do corte'; o aviso ao aparelho de quem testa."* Antes do passo 12, portanto, e nos dois caminhos (a tela nova e o
PDV antigo): duas vendas, as duas canceladas. A unidade se combina antes da janela, e o cadastro de quem testa tem de
existir nela, com o telefone dele. Pela leitura do código, a venda cancelada (a situação 0) sai da consulta do NPS; **se
ela sai também do retrato, da comissão e dos pontos, o ensaio da sua 199 (a) mede** (o item 12 de "O que falta"): o
retrato das 05:25 roda depois do passo 12, e a fatura do dia 16 soma o retrato.

**E o que o cancelar não desfaz**, pela leitura do código (o `finalizar` de `module/ISVenda/src/Service/VendaPagamentoService.php`
e o `cancelar` de `module/ISVenda/src/Service/VendaService.php`): fechar a venda num cadastro que não é o avulso também
dá a comissão da indicação a quem indicou — crédito em dinheiro da unidade, no `ClienteCredito` dele, quando é a
primeira venda finalizada de um cadastro indicado —, o cupom do sorteio ativo da unidade e o cupom do checkout. O
`cancelar` devolve o caixa, o estoque, os pontos, os pacotes e o crédito, e não esses três. **Por isso o cadastro de quem
testa precisa já ter uma venda finalizada, ou não ter indicação à espera**: senão um terceiro, cliente de verdade, ganha
o crédito, e a indicação fica gasta. Se o cadastro de quem testa não cumprir, a escolha volta ao dono. O cupom do sorteio
e o do checkout ficam com quem testa: o ensaio os mede, e o que fazer com eles volta ao dono com a medida. **E o caixa:**
a venda pede o caixa aberto do dia de quem a lança (o **D28**) — quem testa abre o dele na unidade combinada, e o fecha
depois de cancelar.

**Qualquer falha aqui é motivo de volta, e não de análise no meio da janela** — a regra do procedimento. Quanto tempo o
teste leva é a medir: o ensaio dá o número.

### 12. Religar o cron

**O crontab novo entra nesta janela — a sua 194 (a)** — na forma da sua 5 (b): `curl -fsS`, o token no cabeçalho, o `-m`
de cada linha. **Mas sem e-mail: a sua 128 (b), de 12/10/2026, mudou a 31** — *"Sem e-mail: o aviso vai ao log do sistema"*.
A 194 (a) falava em "o endereço que você mandar": a pergunta 194 foi escrita sem lembrar a 128 — ela ainda dizia "Falta o
e-mail que recebe o aviso" —, e nenhum endereço veio. Por isso vale a 128 (b), e o crontab desta etapa não tem `MAILTO`
com endereço. **O passo 9 do procedimento serve assim desde 07/11/2026** (a fatia 113; o item 4 de "O que falta"): o
primeiro bloco escreve `MAILTO=""` e põe em cada linha o `|| logger -t rotinas "falhou (curl $?): <a rota>"`, e a
conferência do segundo o exige — e não exige mais endereço nenhum. O aviso se lê no log do sistema (`journalctl -t
rotinas`), e o 8 de "O que hoje bloqueia" do procedimento confere, antes da janela, que ele chega lá. Então:

- **os dois blocos do passo 9 do procedimento, na forma nova.** Eles leem `/root/crontab-antes-do-corte.txt` (o do passo 1)
  e o `cron.token` do `local.php` — aqui, o do MySQL que o passo 7 pôs no lugar, com o valor sorteado (a sua 193 (b)) —, e é
  exatamente o estado desta etapa. O primeiro bloco começa pelo `curl` do 401;
- **o domínio raiz, `franquiabv.com.br`** (a sua 194 (a), o do `curl` do 401): o `awk` do primeiro bloco troca o `www`
  pelo raiz, e a conferência do segundo recusa a linha fora dele (desde 07/11/2026, a fatia 113; o 4 de "O que hoje
  bloqueia"; `f1-6-agendamento.md`);
- **depois do `crontab`**, as linhas de `crontab-a-aplicar.md` (`crontab -e`): a dos convênios e as das duas limpezas da
  sua 127 (a), que entram nesta etapa pela sua 195 (a) — a da fila, de 24 horas, que **poupa a mensagem que falhou** (a sua
  184 (a): ela sai em 7 dias, contados do pedido, pela sua 185 (a), no `CronJobService::marketingPush`, que já a apaga pelo
  `PushAppRepository::apagarFalhasAntigas`), e a do arquivo de exclusões, de 60 dias. **As duas rotinas existem desde
  07/11/2026** (a fatia 113; `pushAppLimpar` e `agendasExclusoesLimpar`, o item 3 de "O que falta"); as linhas, movidas
  para a tabela de `f1-6-agendamento.md`, e `php scripts/conferir-agendamento.php` sem as duas no `--conhecidas=` do
  `scripts/conferir-tudo.sh`.
  A linha do parabéns não muda: a sua 186 (c), "Como está: 100 de cada por dia" — e, para ela continuar "como está", a da
  fila roda ao menos uma vez entre as 24 horas das filas do parabéns e do "sentimos sua falta" e o envio das 10:00 do dia
  seguinte (de hora em hora, como o evento, cumpre; o item 3 de "O que falta");
- **os outros agendadores que o passo 1 parou** (a leitura 11) voltam, cada um como foi guardado. O passo 9 só converte o
  crontab do root: se algum deles chamar `/api/cronjobs`, `/api/marketing` ou `/api/cupons` com o token na URL, ele daria
  401 com o `cron.token` novo — o que fazer com ele volta ao dono **antes** da janela, com a leitura na mão. E a rotina do
  dump de hora em hora volta aqui, como foi guardada no passo 1 (a sua 204 (a); o passo 3);
- **o evento de hora em hora desligado, só depois de as duas rotinas estarem no crontab** (a sua 195 (a): "desligada na
  mesma janela (reversível: a volta a religa)"): `ALTER DEFINER=<o da leitura 5> EVENT limpar_push_app_24h DISABLE;` no
  `franquia_producao` (no dump de 16/08/2026, `` `andre`@`%` ``), e a consulta da leitura 5 (`information_schema.EVENTS`)
  antes e depois — o `STATUS` em `DISABLED`, e o `DEFINER` o mesmo. **Sem a cláusula, o `ALTER` troca o `DEFINER` pela
  conta que o roda** (medido em 07/11/2026 num banco descartável do MySQL 8.4.13 local: `franquiabv@%` virou
  `root@localhost` no `DISABLE`, e ficou assim no `ENABLE`; com `ALTER DEFINER=...`, ficou o de antes). **A cláusula pede
  um privilégio a quem a roda**: o `SET_ANY_DEFINER` no MySQL 8.2 em diante (no 8.4 o `SET_USER_ID` nem existe mais), o
  `SET_USER_ID` no 8.0, ou o `SUPER` — a versão de cada servidor é a leitura 1. **Sem ele, o `ALTER` com a cláusula
  recusa e nada muda** (medido em 07/11/2026, de novo num banco descartável do 8.4.13 local, com uma conta só com o
  banco e o evento de outra conta, como o do `andre`: `ERROR 1227 ... SUPER or SET_ANY_DEFINER`, e o evento seguiu
  `ENABLED`, com o mesmo `DEFINER`; o `ALTER EVENT` sem a cláusula, pela mesma conta, desligou **e trocou o `DEFINER`
  pela dela**; e a volta com a cláusula, pela mesma conta, também recusou — só uma conta com o privilégio devolveu o
  dono de antes). **Por isso o privilégio se confere antes da janela** (`SHOW GRANTS` da conta que vai rodar o passo,
  com a leitura 5; o item 3 do que só a produção diz, abaixo), e **na janela, se o `ALTER` recusar, PARE esta linha: o
  `ALTER EVENT` sem a cláusula NÃO se roda** — trocar o dono de um objeto do banco de produção não é recurso do roteiro, e a
  volta, sem o privilégio, não o devolveria. O evento fica ligado, como hoje (as duas rotinas novas rodam junto: a falhada
  sai em 24 h, como hoje, até ele se desligar), e o que fazer volta a você (a pergunta do **D26** no lote, de 07/11/2026).
  A volta o religa com `ENABLE`, na mesma forma. O `evt_agendas_exclusoes_limpar` fica como está (`SLAVESIDE_DISABLED`, nunca rodou: **D26**);
- **"Confira depois de 10 minutos"**, a conta do procedimento: só `" 200 "`, e mais de zero.

**Se o crontab novo não puder entrar, o recuo** — o caminho que a sua 193 (b) previu, *"e, se o crontab novo não puder
entrar [...], o token de hoje, porque o crontab de hoje o manda"*. **Ele é a reserva da janela, e não a falta do item 4:**
a sua 194 (a) pôs o crontab novo nesta etapa, e a janela não começa sem ele pronto e ensaiado (o item 8 de "pronto"). A
193 (b) deu o recuo para "se o crontab novo não puder entrar (falta o e-mail do aviso)"; o e-mail saiu pela sua 128 (b), e
o que ainda pode impedi-lo é a própria janela — a conferência do segundo bloco recusar uma linha do crontab de lá numa
forma que ela não conhece, e a correção à mão não bastar. **O que vem depois de um recuo está decidido: a sua 207 (a)** —
o crontab novo na noite seguinte (abaixo, depois do bloco do recuo); até lá, um dia sem aviso. O segundo bloco não instala, de propósito, e não grava a marca que a conta dos 10 minutos lê
(`/root/nginx-antes-do-cron.txt`, só no ramo que instala): sem um recuo escrito, a conta dá "nada", e o crontab de antes
manda o pedido com `-s ... > /dev/null 2>&1`, que esconde o 401. Basta o `cron.token` do `local.php` não ser o valor de
hoje — sorteado no preparo, à espera do crontab novo — para as rotinas pararem caladas: o retrato das 05:25, o NPS e a
fatura do dia 16. E o crontab de antes traz as quatro rotinas que saíram do código (as duas de 13/10/2026, a das 03:10, `relatoriosFidelizacaoAtualizar`, em 25/10/2026, e a da Evasão das 04:10, `relatoriosEvasaoAtualizar`, em 04/11/2026), que dariam 404 todo dia,
calado. As linhas de `crontab-a-aplicar.md` (os convênios e as limpezas) entram do mesmo jeito, com o token de hoje; o
evento só se desliga com as limpezas no crontab. **E o recuo não avisa de nada:** as linhas de antes calam o erro (o
`2>&1` para o `/dev/null`), e o aviso no log da sua 128 (b) só vem com o crontab novo — até ele, só a conta dos 10
minutos e o `data/logs/log_scripts.txt` dizem que as rotinas rodam. O recuo, que só instala com o token batendo:

```bash
cd /var/www/franquiabv.com.br/public_html
umask 077
grep -vE '/api/cronjobs/(relatoriosDashboard(Colaboradores|FluxoMovimento)|relatoriosFidelizacao|relatoriosEvasao)Atualizar[?"]' /root/crontab-antes-do-corte.txt > /root/crontab-recuo.txt
CRON_TOKEN=$(php -r '$c = require "config/autoload/local.php"; echo $c["cron"]["token"] ?? "";'); export CRON_TOKEN
if awk '!/^[ \t]*#/ && /curl / { c++; if (ENVIRON["CRON_TOKEN"] != "" && index($0, "token" "=" ENVIRON["CRON_TOKEN"] "\"")) t++ }
        END { printf "%d curl, %d com o cron.token do local.php\n", c, t; exit !(c > 0 && t == c) }' /root/crontab-recuo.txt
then
    wc -l /var/log/nginx/*.log > /root/nginx-antes-do-cron.txt   # a mesma marca do segundo bloco do procedimento
    crontab /root/crontab-recuo.txt && echo "crontab de antes INSTALADO, sem as quatro que sairam"
else
    echo "NAO INSTALADO: o crontab de antes nao manda o cron.token do local.php -- as rotinas dariam 401 caladas"
fi
unset CRON_TOKEN
```

Nenhuma linha imprime o token. O primeiro número tem de ser o das linhas de `curl` do crontab guardado menos as quatro (21
pela conta de hoje). `NAO INSTALADO` é voltar ao passo 7 e pôr no `local.php` o valor de hoje (o `crontab -l` guardado o
tem; com o crontab novo pronto, o `local.php` traz o valor sorteado, e o recuo sempre passa por aqui), **recarregar o
PHP-FPM** (o passo 9) e rodar o bloco de novo. Sem o recarregar, com o `opcache.validate_timestamps` desligado (a leitura
7), o php-fpm segue com o `local.php` que já leu: a conferência — que lê o arquivo pela linha de comando — passa, e todas
as rotinas dão 401, caladas pelo `> /dev/null 2>&1`; só a conta dos 10 minutos acusaria. Depois, a linha de `crontab-a-aplicar.md` e a conta dos 10 minutos, como no caminho de cima:
ela lê a mesma marca, e o filtro dela casa também com o pedido de `?token=` na URL. **Não ensaiado como bloco:** o
`scripts/ensaiar-crontab-do-passo-9.sh` ensaia os dois blocos do procedimento e, desde 25/10/2026, a linha do `grep -vE`
deste (tirada daqui, sobre o mesmo crontab inventado: tira as quatro e deixa as três irmãs que ficam, a das 05:35, a das
23:50 e a do ticket médio das 04:10), e não o bloco inteiro. A conferência deste rodou só em 14/10/2026, sobre um crontab inventado de quatro linhas,
no gawk do Git Bash e no mawk do contêiner: passa com o token certo e recusa outro token e o vazio.

**Depois de um recuo, o crontab novo na noite seguinte — a sua 207 (a)**, *"O recuo na janela, e o crontab novo na noite
seguinte: depois das 00:05, o mesmo bloco do passo 12 — o cron parado, o token novo, o recarregar e a conferência —, e a
conta dos 10 minutos; o site fica no ar (só o cron muda, e o recarregar corta o pedido que estiver em curso, de madrugada
quase nenhum). Até lá, um dia sem aviso."* Na árvore do código novo, que está no ar, e na ordem que não deixa rotina
nenhuma bater no token trocado:

```bash
# na noite seguinte ao recuo, depois das 00:05 (a sua 207 (a)) -- o site fica no ar, sem a manutencao
cd /var/www/franquiabv.com.br/public_html
umask 077                                                        # o crontab do recuo tem o token na URL: so o root o le
crontab -l > /root/crontab-do-recuo.txt && crontab -r          # 1. o cron parado: o recuo sai, guardado para voltar a ele
sh scripts/sortear-token-cron.sh config/autoload/local.php       # 2. o token novo no local.php, sem imprimir (a sua 193 (b))
#    3. recarregar o PHP-FPM (o passo 9): sem ele o php-fpm segue com o token de antes e o crontab novo da 401
#    4. os dois blocos do passo 9 do procedimento, como no caminho de cima deste passo: leem o
#       /root/crontab-antes-do-corte.txt da janela e o cron.token novo; o domínio raiz; e as linhas de crontab-a-aplicar.md
#    5. a conta dos 10 minutos: só " 200 ", e mais de zero
```

O evento de hora em hora já está desligado desde a janela (as limpezas entraram com o recuo), e segue desligado. **Se o
bloco não passar, de volta ao recuo:** o `local.php` com o token de antes (o do `crontab -l` guardado), o recarregar, e
`crontab /root/crontab-do-recuo.txt` — e mais uma noite sem aviso, com o motivo anotado para a resposta seguinte do dono.
O que a rotina em curso perde com o recarregar é o que a 207 (a) aceita: de madrugada, quase nada — as que podem estar
rodando depois das 00:05 são as de 5 e de 10 minutos. **Não ensaiado** — os dois blocos do passo 9 sobre o crontab de antes são os que o `scripts/ensaiar-crontab-do-passo-9.sh`
ensaia, e o resto é este texto.

### 13. Liberar

Tirar a página de manutenção. **O horário limite está combinado: a sua 190 (a)**, "se às 5h não estiver liberado,
voltamos" — e a janela termina antes das 05:25 (a linha do tempo, abaixo).

---

## A linha do tempo da janela

**A sua 190 (a):** uma noite de dia útil, fora dos dias 15 e 16 e da virada do mês, **começando depois das 00:05** e
**terminando antes das 05:25**, com o limite combinado antes — *"se às 5h não estiver liberado, voltamos"*. São 4 h 55 min
das 00:05 às 05:00 para os treze passos, e 25 min das 05:00 às 05:25 para a volta, se ela vier no limite.

| quando | o passo | o que o repositório tem medido | o que só o ensaio da sua 199 (a) mede |
|---|---|---|---|
| 00:05 | o começo | as rotinas das 23:40, 23:50 e 23:58 já terminaram: o `comandasCancelar` das 23:58 tem 300 s (`f1-6-agendamento.md`) | — |
| | 1. parar o cron e trazer a release | o `crontab -r` é imediato | a cópia do pacote e do `local.php` para a pasta ao lado (a sua 201 (a)): o tamanho do pacote não está medido |
| | 2. a manutenção, e a espera | **até 590 s** depois do último disparo antes do `crontab -r`: o `marketing/envios`, de 10 em 10 minutos, tem `-m 590`, e o worker segue depois de o `curl` desistir (o **D67**) — um teto conservador: entre as 20:00 e as 09:00 ele responde "Fora do horário" sem tocar em nada (o `foraDoHorarioDasCampanhas`, o **D301**), e o que pode estar em curso às 00:05 são o `npsNotificacoes` e o `npsContabilizar`, de 180 s | quanto a espera dura de verdade — o sinal é o `SHOW PROCESSLIST`, e não o relógio |
| | 3. o backup | — | o dump da rotina disparado à mão (a sua 204 (a); a base do teste tem cerca de 2,3 GB), e a cópia dele para fora do servidor e da rotação |
| | 4. o passo 0 | o índice de `id_pai`, de 11 a 20 s, e o da grade, criar, de 11 a 20 s (**D189**); as duas `INSTANT`, 2 s cada na cópia; os cupons, 1 s no servidor da cópia (100.375 linhas) | **as duas cópias grandes, `vendas` (2,2 milhões de linhas) e `pontuacoes_historicos` (1,18 milhão), pelo `COPY`, no banco descartável**; os cupons no tamanho da produção; o remover do índice antigo da grade; e o atraso das três réplicas |
| | 5. a semente | — | a medir |
| | 6. o Líder e a cópia das senhas | uma linha no Líder; na cópia, um `UPDATE` sobre `usuarios` | a medir, e não deve pesar |
| | 7. trocar o código, a linha do cancelamento e o consumo refeito | a linha: um `INSERT` de uma linha; o consumo dos clientes refeito (`scripts/refazer-consumo-dos-clientes.sql`, a sua 295 (a)): cerca de 19 s num clone descartável da cópia, com o `UPDATE` dos 9.184 (o **D698** e o **D707**); a pontuação recalculada (`scripts/recalcular-pontuacao-no-corte.sql`, as suas 230 (a) e 123 (a)): cerca de 3 s num clone descartável das duas tabelas da cópia, com 1 linha criada e 7.794 recalculadas (o **D284** e o **D312**) | o `cp -aT` do `data/` (o tamanho do de lá é a medir); as trocas de nome e as pastas com `mv -T` são no mesmo disco, sem cópia |
| | 8 e 9. o nginx e o PHP-FPM | — | a medir |
| | 10. conferir | — | a medir |
| | 11. o teste à mão | — | **a medir**: cinco papéis, duas vendas canceladas, o push |
| | 12. religar o cron | **10 minutos, no mínimo**: a conta do procedimento | o resto do passo |
| | 13. liberar | — | — |
| 05:00 | o limite | liberado, ou a volta começa | — |
| antes das 05:25 | a volta, se veio | se o passo 12 já rodou, **a espera das rotinas que o crontab novo disparou** — até 600 s, as das 04:10 —, como a do passo 2 ("A volta", abaixo) | **o tempo da volta inteira**, de dentro da manutenção até o login do código antigo e o crontab de antes — sem aquela espera: no BVIP não roda cron |

**Cabe?** O que está medido soma pouco mais de 20 minutos — a espera de até 590 s, o minuto e pouco que se conhece do
passo 0 e os 10 minutos da conta do cron — e cabe com sobra nas 4 h 55 min. **Mas se a janela cabe não se sabe:** o
dump, as duas cópias grandes do passo 0, a cópia da release e o teste à mão nunca foram medidos no tamanho da produção, e
são eles que pesam. A conta, com os números do ensaio na mão (o mais lento das duas vezes, e a folga de que o procedimento
fala, "porque algo vai dar errado"): **os passos 1 a 13 têm de terminar antes das 05:00, e a volta tem de caber nos 25
minutos entre as 05:00 e as 05:25.**

**Se não couber:**

- **no ensaio, os passos 1 a 13 além das 05:00:** a janela não começa como está escrita, e os números voltam ao dono — o
  que muda é escolha dele;
- **no ensaio, a volta além de 25 minutos** (somada à espera das rotinas, que o ensaio não mede): o limite das 5h deixa a
  janela passar das 05:25 numa volta. **O limite fica** — a sua 209 (a) escolheu o retrato à mão, e não o limite mais cedo
  (a opção (b) da pergunta) —, e o retrato pulado se refaz no mesmo dia (abaixo); o número vai ao dono com o ensaio, como
  todos;
- **na janela:** o limite é o das 5h, e a volta é a de "A volta", de dentro da manutenção. Se num passo o que falta, pelo
  tempo do ensaio, já não cabe até as 05:00, a resposta das 5h já se sabe ali — e a volta que começa antes termina antes.

**O que a janela pula** são as rotinas entre as 00:05 e o passo 12 (`f1-6-agendamento.md`): as de 5 e de 10 minutos, e,
conforme a hora do passo 12, as das 01:10 (`clientesAtualizarConsumo`, os últimos 2 dias), 01:20
(`cupons/cuponsValidarVencimento`), 02:10 (`relatoriosAtendimentosAtualizar`), 03:10 (`relatoriosFidelizacaoAtualizar`, os
últimos 3 meses, no código antigo, se voltou: ela saiu do código novo em 25/10/2026), 04:10 (`unidadesTicketMedioAtualizar`, e `relatoriosEvasaoAtualizar` no código antigo, se voltou: ela saiu do código novo em 04/11/2026) e 05:15
(`npsContabilizarUnidadesColaboradores`). Terminando antes das 05:25, **o retrato dos royalties das 05:25 e o faturamento
das 05:35 rodam na mesma noite**, no código que estiver lá: o novo, se liberou; o antigo, se voltou.

**Passando das 05:25 com o cron parado, o retrato da véspera se perde, e não se recupera sozinho.** O `royaltiesAtualizar`
(`module/ISRelatorio/src/Service/FranquiaService.php`) só recalcula ontem e hoje, e a rodada do dia seguinte já não
alcança a véspera da janela: um dia útil inteiro fica com o que a rodada da manhã dele viu às 05:25 — quase nada —, e a
fatura do dia 16 soma o retrato e sai a menos para todas as unidades. O das 05:35 (o `relatoriosDashboardFaturamentoAtualizar`,
os últimos 3 meses) se recupera na noite seguinte.

**Se o retrato for pulado: à mão, no mesmo dia — a sua 209 (a)**, *"O retrato à mão, no mesmo dia: depois de liberar, no
código que ficou no ar, você ou quem você indicar dispara uma vez a rotina das 05:25 (a mesma linha do crontab), antes da
meia-noite; ela refaz a véspera e o dia, e nada mais."* A rotina é a `relatoriosRoyaltiesAtualizar` (a tabela de
`f1-6-agendamento.md`), e **não fala com o Asaas** — as duas da fatura falam, e essas nunca se chamam à mão (o
`relatoriosRoyaltiesFaturasAtualizar` e o `relatoriosRoyaltiesFaturasGerar`).

**Pulado é o dia em que o crontab só voltou depois das 05:25, e não o dia em que se liberou depois delas.** Às 05:25 o
cron dispara a rotina se houver um crontab instalado — o novo do passo 12, o do recuo, ou o de antes, que o item 5 da volta
repõe **antes** de liberar —, e a manutenção deixa o servidor passar: com o crontab de volta às 05:20 e a liberação às
05:27, a rotina das 05:25 rodou, no código que estava lá, e **nada se roda à mão**. Anote a hora de cada `crontab` da
janela e da volta, e confira pelo log do nginx, que guarda o pedido das 05:25 (só a contagem: o token da linha de antes,
na URL, não se imprime):

```bash
# o pedido das 05:25 de hoje, pelo codigo HTTP -- o nginx o registra quando a resposta termina (ate as 05:35, o -m 600)
HOJE=$(LC_ALL=C date +%d/%b/%Y)
cat /var/log/nginx/*.log /var/log/nginx/*.log.1 2>/dev/null | grep -F '"GET /api/cronjobs/relatoriosRoyaltiesAtualizar' \
  | grep -F "[$HOJE:05:" | grep -oE '" [0-9]{3} ' | sort | uniq -c; unset HOJE
```

| a contagem mostra | quer dizer |
|---|---|
| **nada** | o cron não disparou às 05:25: **pulado**, e o bloco de baixo roda — se a hora anotada do `crontab` também diz depois das 05:25. Sem a hora anotada, ou com o log girado para além do `.1`, confira antes a véspera na tela dos royalties: com o movimento do dia inteiro, ela rodou, e nada se roda |
| `" 200 ` | rodou: nada se roda à mão |
| `" 499 ` | o `curl` desistiu aos 600 s e o processo seguiu (o **D67**): rodou, ou está rodando — nada se roda à mão |
| `" 503 ` ou `" 401 ` | a manutenção não deixou o servidor passar, ou o token não bateu: não rodou, e o bloco de baixo roda, depois de consertar a causa (o 401 é o do passo 12) |

**Uma vez só, e nunca junto com outra.** No código novo a rotina tem trava contra duas rodadas juntas (a
`TravaDeExecucao` do `FranquiaService::royaltiesAtualizar`, o **D44**): a segunda responde `"sucesso": false`, com 200.
**No código antigo, não** (o `eaf1fca` não tem trava nas rotinas de royalties: "Um sistema só gravando", acima): duas
rodadas juntas leem "não existe" na linha de cada unidade e gravam as duas, e a fatura soma as duas (o **D71**). Por isso o
bloco só roda com a contagem de cima dizendo que a das 05:25 não disparou, e não se repete — nem com a saída 28, que é o
`curl` desistindo aos 600 s com o processo ainda rodando. **No dia 16, antes das 09:00:** a volta não tem a restrição de
dia da janela, e, na noite de 15 para 16, a rodada pulada é a do dia 15, o último do ciclo; às 09:00 o
`relatoriosRoyaltiesFaturasGerar` soma o ciclo e cria a fatura, e a fatura criada não se refaz (o `if (empty($fatura))`
do `FranquiaService::relatoriosRoyaltiesFaturasGerar`): o retrato à mão depois das 09:00 deixaria todas as unidades
cobradas a menos por um dia. O "antes da meia-noite" da 209 (a) vale em todo outro dia. A linha é a do crontab que estiver
instalado — o novo, se liberou; o de antes, se voltou —, e o comando dela roda uma vez, no servidor, sem imprimir o token:

```bash
# no servidor, depois de liberar e antes da meia-noite (no dia 16, antes das 09:00), UMA vez (a sua 209 (a)), e so com a
# contagem de cima dizendo que a das 05:25 nao disparou -- so a das 05:25, nunca as da fatura
crontab -l | grep -c '/api/cronjobs/relatoriosRoyaltiesAtualizar'   # tem de dar 1: a linha da rotina, e nenhuma outra
crontab -l | grep '/api/cronjobs/relatoriosRoyaltiesAtualizar' | grep -c '%'   # tem de dar 0 (abaixo)
LINHA=$(crontab -l | grep '/api/cronjobs/relatoriosRoyaltiesAtualizar' | sed -E 's/^([^[:space:]]+[[:space:]]+){5}//')
sh -c "$LINHA"; echo "saida: $?"; unset LINHA                       # o comando da linha, sem os cinco campos da hora
```

O `grep -c` diz que a linha é uma só, e o `grep` casa só a das 05:25 (`relatoriosRoyaltiesFaturasAtualizar` tem o
`Faturas` no meio). O `%` numa linha do crontab é o fim do comando para o cron, e o `sh -c` o leria de outro jeito: com
ele, pare — o comando não seria o que o cron roda (nenhuma das duas formas de hoje o tem; se a do aviso no log, a escrever
no item 4 de "O que falta", o tiver, este bloco muda junto). A linha do crontab novo traz o token no cabeçalho, que o `local.php` dá (o passo 12); a de antes, na
URL — nos dois casos ele fica dentro da variável, e nada o imprime. **A saída não confere a rodada:** a linha de antes
(`-s`, sem `-f`, com `> /dev/null 2>&1`) sai 0 sempre, e o código antigo responde `{"sucesso": true}` com ou sem rodar;
a nova sai 0 também quando a trava recusou (o 200 com `"sucesso": false`); e 28 é o `curl` que desistiu, com o processo
rodando. Nenhuma das duas escreve no `data/logs/log_scripts.txt` (a rotina de royalties não chama o `logScripts`, nem no
código antigo nem no novo). **Confere a tela dos royalties:** a véspera de cada unidade com o movimento do dia inteiro, e
não o quase nada que a rodada da manhã dela viu — e, se a rotina ainda roda (a 28), espere a tela, e não rode de novo.
**Não ensaiado** — no BVIP não roda cron, e a rotina não se chama aqui (ela grava royalties de verdade).

---

## Depois da janela

**Acompanhe 48 horas, e de novo na fatura do dia 16** — a sua 190 (a) —, e mais as primeiras vezes de cada rotina que
fala com fora. Quem acompanha é o dono, ou quem ele indicar. O aviso de erro das rotinas está no log do sistema (a sua 128
(b): "alguém tem de olhar o log"), na forma que o item 4 de "O que falta" escrever. Os horários são os da tabela de
`f1-6-agendamento.md`:

| quando | o quê | onde se vê |
|---|---|---|
| 10 minutos depois do passo 12 | `npsNotificacoes`, `npsContabilizar` e `marketing/envios` — o `marketing/envios`, entre as 20:00 e as 09:00, responde "Fora do horário" com 200 e não manda nada (`CronJobService::marketingEnvios`, o `foraDoHorarioDasCampanhas`, o **D301**): a conta dos 10 minutos mostra o 200 sem envio nenhum | a conta do passo 9 do procedimento |
| a primeira rodada da limpeza da fila | a limpeza de 24 horas, no crontab, no lugar do evento desligado (a sua 195 (a); de hora em hora, ou ao menos uma vez entre as 24 horas das filas do dia anterior e o envio das 10:00 — o item 3 de "O que falta"): a pendente de mais de 24 horas sai, e a falhada fica até os 7 dias (a sua 184 (a)) | a fila `push_app`, e o log do sistema |
| a primeira madrugada | das 01:10 às 05:35, o que a janela não pulou (a linha do tempo, acima): o consumo, os cupons, os relatórios, o NPS por unidade, o retrato dos royalties das 05:25 e o faturamento; e a limpeza do arquivo de exclusões, de 60 dias | `data/logs/log_scripts.txt`, e o log do sistema |
| 08:00 | o `marketingPush`, de minuto em minuto das 8h às 22h, manda a fila geral — a pesquisa do NPS que o `npsNotificacoes` pôs nela, pelo WhatsApp, e o push: **o WhatsApp de verdade, pela primeira vez no código novo**, sem a conta dos 10 minutos para olhar | a fila `push_app`, e o log do sistema |
| 09:00 | o `marketing/envios` passa a mandar pelo WhatsApp da unidade as campanhas que estiverem na hora — **e não manda a que passou do prazo** (a fatia 92b; o D609): a `agendado` com mais de 3 horas desde o começo do prazo dela fica "Atrasada" na lista, para alguém mudar a hora ou cancelar, e a de push agendada antes da troca segue a mesma regra (só se cancela); quantas são, a leitura das campanhas na fila do passo 7 disse. O mesmo cliente não recebe duas campanhas no dia, e a unidade tem o limite do dia (a sua 260 (c)). **Na volta**, o código antigo não tem o prazo: a atrasada sai na primeira rodada | as campanhas na tela (a coluna Status), e o log do sistema |
| 06:00, 07:00 e 10:00 | o parabéns e o "sentimos sua falta" entram na fila, e a chamada das 10:00 (`marketing/marketingPushOutros`) os manda — 100 de cada por dia, como hoje (a sua 186 (c)) | a fila `push_app` |
| 12:30 depois da janela | `relatoriosRoyaltiesFaturasAtualizar`: a primeira conversa do código novo com o Asaas — a que a sua 198 (a) espera antes de o dono poder declarar o fim da volta | o log do sistema, e as faturas na tela |
| 23:40 | `yungasFaturamentoAtualizar`: o Yungas | o log do sistema |
| 09:00 do dia 16 | `relatoriosRoyaltiesFaturasGerar`: a primeira fatura emitida pelo código novo | as faturas na tela e no Asaas |
| nas 48 horas | as três réplicas: os dois fios rodando, sem erro, e o atraso zerado depois das cópias do passo 4 (a leitura 21) | `SHOW REPLICA STATUS` em cada uma |

**O convênio das vendas antigas é acerto do dono, fora da janela -- a sua 358 (c), o D632 (a fatia 132, 07/11/2026).**
Desde o código novo, o lançamento grava na venda o convênio do item que recebe o desconto; as vendas de antes ficam como
estavam até você rodar, quando quiser, o roteiro que grava nelas o convênio pelo nome do motivo do último item do
convênio (as que citam um nome que não existe mais, ou que dois convênios têm, ficam). Ele conta antes, lista cada venda
que muda com o convênio de antes -- o caminho de volta --, grava só `vendas.convenio` e confere no fim; rodado de novo, não
muda nada. **E a "última venda" dos convênios** (a sua 363 (a), a fatia 136, 08/11/2026): o convênio que passa a receber
uma venda concluída acertada mais nova que a "última venda" gravada (ou sem nenhuma gravada) passa a essa data, nunca
para trás, com a lista do convênio, da data de antes e da nova, antes da escrita. Não é passo da janela, e nada nela
depende dele; depois dela, o código novo já não cria vendas assim (desde a 136, também a venda que troca de cliente ou
perde o item do convênio revê o dela). Na cópia de produção, 1.181 vendas sem convênio e 46 com outro mudariam, e 8
convênios teriam a "última venda" avançada. Com o arquivo copiado para o servidor:

```bash
mysql franquia_producao < acertar-convenio-das-vendas.sql | tee /root/acertar-convenio-das-vendas-<data>.txt; echo "o mysql saiu com ${PIPESTATUS[0]}"   # D632
```

**As entregas depois da janela — a sua 200 (c), "Recebe as entregas normalmente desde o dia seguinte".** E a sua 198 (a)
quer, até o dono declarar o fim da volta, "nada irreversível". **As duas cabem com esta regra, até a declaração:** só
entra entrega cuja migração **só acrescenta e deixa o código antigo funcionando** — tabela nova, ou coluna anulável ou com
padrão, que o `INSERT` do código antigo não precisa preencher —, e nunca uma que apague linha, mude coluna que ele lê,
troque credencial ou rode o resto do passo 3; cada migração entra no passo 0 do procedimento como as doze (o portão do
passo 0 exige: linha `| 0 |`, laço, `mysql.sql` e o laço do ensaio geral juntos) e é aplicada na produção antes do código
que a usa; e cada entrega sai pelo roteiro de publicar, **com a sua própria volta, à liberação anterior** (o item 9 de "O
que falta"), guardando a árvore antiga desta janela (`public_html.antigo-<data>`) para a volta ao código antigo. Cada
entrega também acrescenta uma linha à tabela "O que fica no banco", da volta.

**Quando e como a troca acontece — a sua 206 (a), "À noite, como a janela"**: *"depois das 00:05 e antes das rotinas da
01:10, com a página de manutenção, o cron parado e a espera das rotinas em curso; alguns minutos por entrega, e nenhum
pedido cortado."* Cada entrega é uma janela pequena, com os passos desta: o 1 e o 2 (o `crontab -l` guardado e o
`crontab -r`, a manutenção, e o `SHOW PROCESSLIST` sem o usuário da aplicação — e sem o dump de hora em hora em curso, o
`pgrep` e o usuário do dump de "1 e 2", porque a migração da entrega também pode copiar tabela), a migração da entrega, se
houver (antes do código), a troca da árvore e o recarregar (os passos 7 a 9, pelo roteiro de publicar), a conferência sem
escrever (o 10), o crontab de volta e liberar. **O crontab desta noite vai num arquivo desta noite** —
`crontab -l > /root/crontab-entrega-<data>.txt`, e o roteiro o recebe em `CRONTAB_GUARDADO` —, e **nunca** no
`/root/crontab-antes-do-corte.txt` da janela: esse é o que a volta ao código antigo religa até a declaração da 198 (a), e
gravar o desta noite por cima dele o perdia (o roteiro recusa o da janela numa entrega, e o guardado de mais de 12 horas).
O que dispara entre as 00:05 e o fim dela são as rotinas de 5 e de 10
minutos, que rodam de novo na rodada seguinte. Quem roda é o dono, ou quem ele indicar. **O tempo de uma entrega, o ensaio
do roteiro de publicar mede** (o item 9 de "O que falta").

**O teto curto do pool do site é uma dessas entregas — a sua 228 (a)**: *"O tempo curto do site só depois de você
declarar o fim da volta: até lá, os dois grupos com o tempo das rotinas; depois da declaração, o tempo curto entra numa
entrega à noite, como as outras."* Ela não tem migração nem troca de árvore: o arquivo do pool do site com o teto curto do
requisito 2 do D67 — escrito antes da janela, com os dois do passo 8 (o item 14 de "O que falta") —, o recarregar do passo
9 e a conferência do 10, na noite, com o cron parado e a espera das rotinas em curso, como as outras. **Antes da
declaração, não entra:** numa volta, o vhost de antes manda as rotinas ao pool do site, e o teto curto as mataria nele,
toda noite, caladas (o **D497**). Até lá, um pedido do site travado pode prender um processo do site por até meia hora.

**A entrega termina antes das 01:10, com a volta dentro** — a letra da 206 (a) a põe "antes das rotinas da 01:10", e é o
crontab de volta antes delas que faz o `clientesAtualizarConsumo` das 01:10 e o `cupons/cuponsValidarVencimento` das 01:20
rodarem na hora. **O limite se combina como o da janela**, que a mesma resposta chama de molde (o "se às 5h não estiver
liberado, voltamos" da sua 190 (a)): a entrega tem de estar liberada até 01:10 menos o tempo da volta à liberação anterior
— a árvore anterior, o recarregar, o crontab guardado e liberar; a migração da entrega fica, porque só acrescenta (a regra
de cima) —, e esse tempo o mesmo ensaio do roteiro de publicar mede. Não liberada nessa hora, volta-se, e a entrega fica
para outra noite; e a entrega que, pelo ensaio, não cabe até essa hora não começa. **Se mesmo assim o cron voltar depois
das 01:10**, o que se pulou se recupera pela coluna da janela de `f1-6-agendamento.md`: o consumo relê os últimos 2 dias e
se refaz na noite seguinte; o vencimento dos cupons, que não tem janela, vence na rodada seguinte o que venceu nesta — um
dia a mais de cupom vencido valendo —; e, depois das 02:10, os relatórios da madrugada, como na linha do tempo da janela.
O número do ensaio volta ao dono, como os da janela.

Pela sua 190 (a), a janela não cai nos dias 15 e 16 nem na virada do mês, e por isso não chega perto das 09:00 do dia 16.
As rotinas que ela pular se recuperam conforme a coluna "janela" da tabela de `f1-6-agendamento.md`: as que releem ontem,
dois dias ou três meses, na noite seguinte; as que trabalham só com "hoje" (o `fidelizacaoAtualizar`) perdem o dia — e
ele roda às 23:50, antes da janela. Quais ela pula depende da hora do passo 12 (a linha do tempo, acima). O retrato das
05:25, pulado, não recupera a véspera ("O que a janela pula", acima), e a fatura do dia 16 soma o retrato: só pesa se o
crontab só voltar depois das 05:25 — na janela ou numa volta —, e aí ele se refaz à mão no mesmo dia (a sua 209 (a); a
linha do tempo) — numa volta na noite de 15 para 16, antes das 09:00 do dia 16, a hora da fatura.

---

## A volta: o código antigo de volta, sobre o mesmo banco

**Nesta etapa não há ponto de não retorno por perda de dado.** Quem grava depois de liberar grava no mesmo MySQL, e a volta
não perde venda. O que ela perde é sentido, e isso cresce com os dias (abaixo). **E ela só está escrita aqui, e não está
ensaiada em lugar nenhum:** a volta do procedimento termina no código novo sobre o MySQL — no estado desta etapa —, e a do
`scripts/ensaiar-corte.sh` é a do PostgreSQL para o MySQL, com o código novo.

**A volta é uma janela também.** Ela vale por dias depois de liberar, com a rede trabalhando: trocar a árvore com o site
no ar mata o pedido que estiver no meio do `mv` e do recarregar do PHP-FPM — uma venda gravada em vários `flush` pode
ficar pela metade —, o que se envia enquanto as pastas de upload voltam cai na árvore errada, e o código antigo passaria a
servir antes de o Líder ser desfeito. "A volta não perde venda" vale para o banco, e só com a volta feita assim:

```bash
# 1. fechar, como o 1 e o 2 da janela: o crontab novo sai primeiro (o codigo antigo nao le o cabecalho), a manutencao
#    entra, e espera-se o SHOW PROCESSLIST sem conexao do usuario da aplicacao, em Query nem em Sleep
crontab -r
# 2. o vhost de antes, COM o bloqueio: a copia do passo 3 traz a manutencao e nao traz o bloqueio
cp <a copia do vhost do passo 3> <o vhost>
sh /var/www/franquiabv.com.br/public_html/scripts/bloquear-migrar-senhas.sh --aplicar   # o roteiro da arvore nova, ainda no lugar
#    a negacao do /index.php/autenticacao/migrar-senhas, a do passo 8, a mao; nginx -t && systemctl reload nginx
nginx -T 2>/dev/null | grep -n 'migrar-senhas'                    # as duas formas -- NUNCA um curl nessa rota
# 3. as arvores, e as pastas de upload com mv -T (o passo 7 ao contrario: na arvore antiga elas nao existem mais)
#    PELO ROTEIRO (o da release, guardado no passo 1): CRONTAB_GUARDADO=/root/crontab-antes-do-corte.txt
#    MANUTENCAO_ARQUIVO=<...> FRONT_RAIZ=<...> bash /root/publicar-producao.sh voltar -- ele recusa sem o bloqueio do
#    item 2 no nginx -T, faz as duas linhas abaixo e o laco com os mesmos nomes, e NAO recarrega o PHP-FPM: o item 4
#    segue igual, e o caminho do devolver-nomes e o que ele imprime. A mao, so sem ele:
cd /var/www/franquiabv.com.br
mv public_html public_html.novo-<data> && mv public_html.antigo-<data> public_html
#    for p in arquivos contratos banners assistentes vip-class visagismo logs layout/img/avatar: o que existir em
#    public_html.novo-<data>/public/$p, mv -T para public_html/public/$p; e o public/app.json, se alguem o mudou no meio
# 4. o Lider desfeito (a linha do passo 6, se o SELECT de antes deu gerente 0); o evento religado, se o passo 12 o
#    desligou (ALTER DEFINER=<o da leitura 5> EVENT limpar_push_app_24h ENABLE, no franquia_producao: sem a clausula,
#    o DEFINER vira a conta que roda -- o passo 12; a clausula recusada, PARE esta linha e nao rode sem ela: o evento
#    fica desligado, a fila sem a limpeza de 24 h ate uma conta com o privilegio religa-lo); as quatro permissoes de volta ao nome
#    antigo, pelo roteiro da arvore NOVA, que agora e a public_html.novo-<data> (a antiga, no lugar, nao o tem):
#    mysql --default-character-set=utf8mb4 franquia_producao < /var/www/franquiabv.com.br/public_html.novo-<data>/scripts/devolver-nomes-das-permissoes-das-telas-novas.sql
#    -- sai com 0; com 1 (o PARE da saida), pare e leia: sem os nomes antigos, o SessaoAcl antigo lanca no login de
#    todos; recarregar o PHP-FPM
# 5. conferir de dentro da manutencao: o npsContabilizar sem token da 200 (o antigo responde sem rodar), e o login entra
crontab /root/crontab-antes-do-corte.txt                         # o de antes, inteiro: o codigo antigo tem as quatro rotinas
#    e os outros agendadores que o passo 1 parou, e a rotina do dump, cada um como foi guardado
# 6. liberar: tirar a manutencao
```

Nada disto foi ensaiado: o ensaio da sua 199 (a) o roda duas vezes, cronometrado (abaixo). O que voltou para a árvore
antiga é o que ela tinha: sem `local.php` e sem a release — a árvore nova fica inteira como `public_html.novo-<data>`, com o
`local.php` dela. As duas limpezas novas saem com o `crontab -r` (o crontab de antes não as tem), e o evento religado volta
a fazer a de 24 horas, que apaga também a falhada, como hoje. **Dentro da janela, antes do passo 13,** a manutenção já
está no ar, e é esta volta que "se às 5h não estiver liberado, voltamos" chama (a linha do tempo). **Antes do passo 12**,
o cron já está parado e nada roda: a espera do item 1 não se repete. **Depois do passo 12, não:** o crontab novo já
disparou rotinas no código novo — as de 5 e de 10 minutos e, conforme a hora, as da madrugada (as das 04:10 têm 600 s) —,
e a volta é a inteira: o `crontab -r` e a espera do item 1, pelo `SHOW PROCESSLIST`, antes de trocar as árvores e de
recarregar o PHP-FPM. Sem ela, a rotina em curso morre no meio, ou termina carregando a árvore antiga — o "metade num
código e metade no outro" de "1 e 2". Essa espera conta nos 25 minutos da volta, e o ensaio não a mede (no BVIP não roda
cron).

**O que quebra a volta, e o que a protege:**

| o quê | com o código antigo | o que protege |
|---|---|---|
| o "Vendas caixa" apagado | ninguém entra | não roda nesta etapa (a sua 192 (a)); se rodasse, as linhas só voltariam do backup, e o roteiro que as repõe não existe |
| o Líder com a visão de gerente | o Líder autoriza fiado e devolve crédito em qualquer unidade, e cancela venda (**D124**) | o inverso do passo 6, no item 4 da volta, antes de liberar |
| as quatro permissões com o nome da tela (o passo 7, a sua 298 (c)) | **ninguém entra**: o `SessaoAcl` antigo pergunta ao ACL pelos nomes antigos no login, e o ACL lança com um nome que a tabela não tem (**D701**) | o inverso do roteiro do passo 7 (`devolver-nomes-das-permissoes-das-telas-novas.sql`, pelo caminho da árvore nova: depois do item 3, ele está em `public_html.novo-<data>/scripts/`, e a árvore antiga no lugar não o tem), no item 4 da volta, antes de liberar; de volta ao código novo, o roteiro roda de novo (o código novo aceita os dois nomes) |
| o dicionário de CEP apagado | sem preenchimento por CEP, de vez (o antigo não tem o ViaCEP) | não roda nesta etapa (a sua 192 (a); a sua 10 (b) fica no passo 3 da etapa 2) |
| o crontab em `Bearer` | o antigo não lê o cabeçalho e, sem `?token=`, responde `{"sucesso": true}` sem rodar (o passo 7 do dia 0): as rotinas param caladas | o crontab de antes, sempre |
| as credenciais rotacionadas | com a senha do MySQL trocada, ele nem conecta; com as dos provedores, as integrações dele param — ele as traz escritas | a rotação das oito só depois de o dono declarar o fim da volta (a sua 193 (b) e a sua 198 (a)); o `cron.token` novo não o afeta, porque ele compara com o valor escrito nele |
| a `/autenticacao/migrar-senhas` | volta viva, e um GET sem login devolveria a senha de antes a quem trocou a sua no código novo (a `senha_php83`, **D79**) | a sua 202 (b), as duas defesas: o bloqueio **reposto** no item 2 da volta, nas duas formas — a cópia do passo 3 é de antes dele —, conferido lendo o vhost; e a `senha_php83` anulada no passo 6, com que a rota não acha ninguém para reescrever |
| o `/novo/` | a tela nova chama a `/adm/api`, que o antigo não tem | o vhost de antes |
| o evento de hora em hora desligado no passo 12 | a fila `push_app` cresce sem a limpeza de 24 h (as duas rotinas novas saem com o crontab novo) | o `ALTER DEFINER=<o da leitura 5> EVENT limpar_push_app_24h ENABLE` no item 4 da volta (a sua 195 (a): "reversível: a volta a religa") |
| as dez rotas do aplicativo | abertas no código antigo, como no novo desta etapa (a sua 189 (b)); o antigo não tem as duas guardas que ficaram (a sua 208 (a)) nem a recusa do colaborador da unidade inativa (a sua 226 (b)), e a volta as perde até o código novo voltar | — |
| o php-fpm e a memória do MySQL, ajustados na janela (a sua 205 (a)) | o antigo roda com eles; com o vhost de antes, as rotinas voltam ao pool de todos — e, se o pool do site tivesse o teto curto do requisito 2 do D67, a rotina que passasse dele morreria nele toda noite, calada | nada a desfazer, como a 205 (a) diz, **porque o pool do site fica com o teto das rotinas até o dono declarar o fim da volta** — decidido em 15/10/2026, a 228 (a) (o **D497**; o passo 8): o teto curto só entra depois da declaração, quando não há mais volta |
| a rotina do dump de hora em hora, parada no passo 1 (a sua 204 (a)) | sem ela, nenhum dump novo depois da volta | religada no item 5 da volta, como foi guardada; o dump da janela segue guardado fora do servidor |
| uma entrega entre as etapas (a sua 200 (c)) | o antigo não sente a migração que só acrescenta; sente a que muda o que ele lê | a regra de "Depois da janela": até a declaração, só a migração que deixa o código antigo funcionando |
| as chaves estrangeiras novas | o antigo não apaga usuário, unidade, produto nem cliente (os `remove()` e os `DELETE` do `eaf1fca`, lidos em 14/10/2026): nenhuma o recusa | — |

**O que fica no banco.** O passo 0 e a semente ficam, e o código antigo não os lê. O que o código novo gravou fica também,
e o antigo o ignora — mas não sem custo. Pela leitura dos dois códigos em 14/10/2026, não medido:

| o que o código novo gravou | o que acontece com o código antigo, e na volta seguinte ao novo |
|---|---|
| a vigência da taxa de royalty (`royalties_aliquotas`) | a taxa mudada no antigo vale no mesmo dia; de volta ao novo, ela é ignorada enquanto a unidade tiver vigência, e o ciclo sai faturado errado |
| a validade da pontuação por unidade | o antigo vence tudo aos 365 dias: a unidade com outro prazo perde pontos, e o novo não os devolve |
| a vigência do horário | o antigo não grava a mudança: de volta ao novo, o conferidor sai com 1, e a vigência só se registra no próximo salvar, com a data do salvar |
| o motivo da quitação em Contas a Pagar | o estorno do antigo reabre a parcela quitada com motivo |
| o hash anterior da unidade | o antigo aceita só o atual |
| o autorizador do fiado e do resgate | o antigo grava sem ele, e o código mestre volta a resgatar a pontuação de qualquer cliente (**D88**) |
| o desconto e o produto do dia do cupom (`cupons_clientes.desconto` e `produto_oferecido`, **D271**) | o antigo não os lê, e o cupom emitido no código novo segue a campanha: se ela mudou depois da emissão, o cliente recebe outro desconto que o prometido |
| quando e quem cancelou a venda (`vendas.cancelado_em` e `cancelado_por`, a fatia 70) e a linha `cancelamento` de `colunas_gravadas` | o antigo cancela sem gravar o instante nem o gerente: de volta ao novo, as canceladas da volta ficam "sem registro" (o `cancelado_em` nulo) DEPOIS da data da linha, e os Cancelamentos no período, o "antes ou depois do fechamento" do Caixa e a lista do Royalties as deixam de fora calados. A linha só se escreve depois do código novo no lugar: nesta etapa, no passo 7, logo depois da troca da árvore e antes de liberar (o 7d do procedimento só a confere na etapa 2), e numa entrega depois da janela, depois da troca da árvore; as colunas de `valorContado`, `motivoDaRetirada`, `descontoManual` e `cotaDoPacote` nascem no passo 0 sem linha, e cada linha vem com o escritor dela (as fatias 71 a 74), pela mesma regra |
| quem lançou o desconto digitado (`vendas_produtos.desconto_lancado_por`, a fatia 71) e a linha `descontoManual` de `colunas_gravadas` | o antigo lança o desconto sem gravar quem o digitou: de volta ao novo, os descontos manuais da volta ficam "não registrado" DEPOIS da data da linha, e o "Por quem lançou" do Descontos os conta na linha "não registrado", e não em quem os lançou. A linha se escreve no passo 7, logo depois da do cancelamento, e numa entrega depois da janela, depois da troca da árvore |
| o motivo da saída de estoque (`produtos_estoque.motivo`, a fatia 73) e a linha `motivoDaRetirada` de `colunas_gravadas` | o antigo faz a saída pela tela de estoque sem pedir o motivo, e a retirada sem gravar `uso`: de volta ao novo, as saídas da volta ficam "sem motivo" DEPOIS da data da linha, e a Retirada de Produtos as conta na coluna "Saída sem motivo", dentro do total -- inclusive um acerto de inventário, que com o motivo ficaria fora; a retirada da volta continua "uso interno" (por quem pediu). A linha se escreve no passo 7, logo depois da do valor contado, e numa entrega depois da janela, depois da troca da árvore |
| o percentual da cota-parte de quem vendeu o pacote (`produtos.cota_parte_pacote`), quem vendeu e a cota dele em cada sessão (`vendas_produtos.vendedor_do_pacote` e `cota_do_vendedor`, a fatia 74) e a linha `cotaDoPacote` de `colunas_gravadas` | o antigo não lê o percentual (o cadastro antigo do pacote de lá não o mostra nem o muda) e finaliza a sessão sem gravar quem vendeu: de volta ao novo, as sessões da volta ficam sem vendedor DEPOIS da data da linha, e a Cota-parte não as credita a quem vendeu -- a cota dele daquelas sessões falta, calada, e a de quem executou não muda. A linha se escreve no passo 7, logo depois da do motivo da retirada, e numa entrega depois da janela, depois da troca da árvore |
| o custo do produto em cada item vendido (`vendas_produtos.custo_unitario`, a fatia 74b) e a linha `custoDoItem` de `colunas_gravadas` | o antigo vende sem gravar o custo: de volta ao novo, os produtos vendidos na volta ficam "sem registro" DEPOIS da data da linha, e o Produtos / Serviços os deixa fora do custo e da margem (a quantidade "sem registro" aparece ao lado), e a Retirada conta essas cortesias pelo custo de hoje do cadastro, como antes. Nada fica errado: só sem margem. A linha se escreve no passo 7, logo depois da da cota do pacote, e numa entrega depois da janela, depois da troca da árvore |
| a porta do convite de indicação (`indicacoes.porta`), as linhas do formulário do passado e as do amigo de quem indica inativo (a fatia 75) e a linha `indicacaoDoFormulario` de `colunas_gravadas` | o antigo não lê a porta e cria a linha do convite só pelo aplicativo -- o padrão da coluna a grava como `aplicativo`, a porta certa --; o amigo do formulário nasce lá só com a marca, como antes da 53 (c), sem linha, e a etapa 3 dele apaga quem indicou: de volta ao novo, esses amigos da volta não aparecem nos convites do formulário (a linha não existe), e a adesão deles não é contada. As linhas do passado e as do amigo de quem indica inativo ficam, e o antigo as mostra: a tela antiga do relatório e a lista de convites do aplicativo (`listarPorClienteIndicador`) leem toda linha, sem a porta -- o convite aparece "aguardando", sem comissão; no código novo a lista do aplicativo também as mostra (a 288 (b) do dono, a fatia 75b: a v4, e a rota antiga com as dez fechadas; com as dez abertas, como sobe a etapa 1, a rota antiga segue só com a ligação até as dez fecharem: a resposta 293 (a) do dono, de 04/11/2026, o D689), e a tela antiga do relatório mostra apenas a linha que é a ligação de um convite. A linha da data se escreve no passo 7, logo depois da do custo do item, e numa entrega depois da janela, depois da troca da árvore |
| os pacotes de cliente de venda já cancelada, encerrados no passo 7 (`scripts/encerrar-pacotes-de-venda-cancelada.sql`, a fatia 74g, a 287 (a)) | ficam encerrados: o antigo lê o pacote pelo `status`, como o novo, e não os oferece; mas o cancelamento antigo não encerra o pacote da venda cancelada na volta -- de volta ao novo, esses pacotes ficam ativos, e o roteiro, rodado de novo depois da troca, os encerra (idempotente). Para desfazer o encerramento da janela, a lista guardada pelo `tee` (`/root/encerrar-pacotes-<data>.txt`, com a `data_alteracao` de antes de cada um) |
| o consumo dos clientes refeito no passo 7 (a sua 295 (a), o **D698**) | fica: o antigo lê o mesmo número, e a rotina de lá o refaz só para quem vem; o cancelamento da volta não grava o `cancelado_em`, e de volta ao novo a rotina não acha esse cliente — fica atrasado até ele voltar, ou até o roteiro do passo 7 rodar de novo (idempotente) |
| a pontuação de todos recalculada no passo 7, e a linha de saldo criada onde faltava (as suas 230 (a) e 123 (a), o **D284** e o **D312**) | fica: o antigo lê a mesma coluna e a recalcula pela mesma soma das linhas `ativa`; mas o cancelamento antigo, na volta, tira reais da coluna, e a indicação de outra unidade volta a ir para a coluna do cadastro de quem indicou — de volta ao novo, essas colunas ficam tortas até o próximo recálculo de cada cliente, ou até o roteiro do passo 7 rodar de novo (idempotente) |
| o dinheiro contado no fechamento do caixa (`caixas.valor_contado`, a fatia 72) e a linha `valorContado` de `colunas_gravadas` | o antigo fecha o caixa sem pedir a contagem: de volta ao novo, os caixas fechados na volta ficam com o contado nulo DEPOIS da data da linha, e o relatório de Caixa os mostra "sem registro" na quebra (nula, e não zero), e o resumo soma a quebra só dos que têm. A linha se escreve no passo 7, logo depois da do desconto manual (e depois da do cancelamento, que a quebra pede), e numa entrega depois da janela, depois da troca da árvore |
| a senha trocada no código novo | a `senha_php83` de todos ficou nula no passo 6 (a sua 202 (b)), e o código novo não a grava (**D79**): a `migrar-senhas` não acha ninguém para reescrever. Quem trocar a senha no código antigo, numa volta, volta a tê-la (o `criptografarSenha` de lá grava as duas), e a rota segue negada no nginx (a tabela de cima). A `salt` (**D78**) guarda a senha de todos nos dois códigos, como hoje (o passo 6) |

**Até quando se volta — a sua 198 (a): "Até você declarar o fim, por escrito: depois das 48 horas e da primeira conversa
do código novo com o Asaas sem defeito que peça volta. Até lá, nada irreversível."** A volta deixa de ser barata quando
roda alguma destas: a rotação das oito credenciais; o "Vendas caixa" ou o dicionário de CEP; uma migração nova, entre as
etapas, que o código antigo não aguente; e o teto curto no pool do site, que numa volta mataria nele, caladas, as rotinas
que o vhost de antes lhe devolve (o **D497**). Por isso, até a declaração, nenhuma delas: a rotação das oito vem depois
dela (a sua 193 (b)), o teto curto também, numa entrega à noite (a sua 228 (a), "Depois da janela"), o resto do passo 3
fica para a etapa 2 (a sua 192 (a)), e as entregas seguem a regra de "Depois da janela" (a sua 200 (c)). O custo de sentido da tabela acima cresce a cada dia mesmo assim. **A única linha da janela que só volta
pelo backup é a da cópia antiga das senhas** (o passo 6, a sua 202 (b)), e ela não pesa na volta: o código antigo só a lê na
rota que o nginx nega. Depois da declaração, a árvore antiga (`public_html.antigo-<data>`) e o dump da janela deixam de
ser o ponto de volta — o que fazer com eles é do dono.

---

## O ensaio da etapa 1

**Decidido: a sua 199 (a)** — *"A troca e a volta no BVIP, duas vezes, cronometradas: com o código antigo lá só durante o
ensaio; e o passo 0 inteiro num banco descartável do BVIP, com vendas e o histórico de pontos copiados sem as duas colunas,
para cronometrar as cópias grandes — a base do teste não muda, e o índice da agenda continua fora dela, como na sua 62."*
**O roteiro dele não existe** (o item 11 de "O que falta fazer antes da janela"): o `ensaio-geral-do-corte.md` é o da etapa
2. O lugar é o ambiente de teste do BVIP (`ambiente-teste.md`), que já roda o código novo sobre o MySQL, e o que se sabe
dele hoje é isto:

- **o que ele prova bem:** a troca da árvore, as pastas de upload, o `local.php`, os proxies, a tela nova, o nginx e a
  manutenção — a mecânica dos passos 2 e 7 a 10 —; e a volta, com as duas trocas de nome ao contrário;
- **o que ele não prova sozinho: o tempo do passo 0.** A base do teste é a cópia de 02/10/2026 (`ambiente-teste.md`), que
  veio da `franquiabv_dev` e **já tem** `vendas.autorizado_por` e `pontuacoes_historicos.autorizado_por`, com a chave
  estrangeira — o código novo que roda lá depende delas. No laço, as duas maiores cópias (2,2 milhões e 1,18 milhão de
  linhas) dizem "já existe", e só a dos cupons (100 mil) copia: a janela sairia dimensionada sem os dois passos mais longos
  dela. Medir os dois pede um banco **sem** essas colunas — o descartável da sua 199 (a), com as duas tabelas copiadas sem
  elas —, montado e apagado no BVIP pelo dono;
- **o que ele não prova:** o cron (lá não roda), os provedores (apontam para o loopback), o aplicativo (não aponta para
  lá), o PHP e o nginx da produção, e os dados que o anonimizador reescreve (os logins `user<id>`, as datas de
  nascimento);
- **o laço inteiro lá aplica mais do que as quatro do `ColunaNoBanco`.** A base de 02/10 não tem: as quatro (os cupons, o
  "Não" do pacote, as saídas e as alíquotas — "na próxima cópia exportada, pela mão do dono", diz o procedimento); o índice
  de `id_pai` (**D220**, de 09/10), que a base de lá ficou sem **por escolha**: "a opção (c), criá-lo também nela, não foi
  escolhida" (a pergunta 62; `ambiente-teste.md`); e a tabela dos aparelhos, se ela não foi criada à mão depois do último
  `base` (`ambiente-teste.md`, "A tabela dos aparelhos do push"). Rodar o laço lá aplicaria cinco ou seis, e não quatro —
  e é por isso que a 199 (a) o põe no banco descartável, e não na base do teste; o índice da grade (a sua 126 (a)), quando a
  migração existir, entra no laço do descartável e na próxima cópia exportada da base do teste, pela mão do dono (a sua 83
  (a)), e não à mão nela;
- **e a volta ao código antigo** lá traz de volta as credenciais de produção escritas nele — só as quatro camadas de "O que
  o ambiente não pode fazer" o seguram: por isso o código antigo fica lá só durante o ensaio.

**O que o ensaio tem de dar.** A sua 199 (a) diz "A troca e a volta no BVIP, duas vezes, cronometradas" e "o passo 0
inteiro num banco descartável": **duas vezes, a troca e a volta**, cronometradas passo a passo; **uma vez, o resto**, no
mesmo descartável quando é dado:

- **a troca (as duas vezes):** os passos 1, 2 e 7 a 10 sobre a base do teste — a cópia da release e do `local.php` para a
  pasta ao lado (o tempo que a sua 201 (a) acrescenta), a manutenção (os três requisitos de "1 e 2"), as trocas de nome, as
  pastas com `mv -T`, o vhost novo com as duas negações e a manutenção, o recarregar e as conferências;
- **o passo 0 no descartável (uma vez):** o laço inteiro, cada migração com o seu tempo — o das duas cópias grandes é o
  número que só ele dá —, e a estrutura conferida depois, como nos ensaios do passo 0 do procedimento;
- **os dados do passo 6 (uma vez, no descartável):** o Líder e a linha que o desfaz; a `senha_php83` anulada (com valores
  inventados: a base do teste já a tem nula), as duas contagens, e o login do código antigo entrando depois;
- **o evento (uma vez, no descartável):** o `ALTER EVENT ... DISABLE` e o `ENABLE`, se o evento existir nele, com o
  `STATUS` e o `DEFINER` antes e depois;
- **a venda de teste da sua 203 (a) (uma vez):** lançada, fechada e cancelada com o motivo "teste do corte", nos dois
  caminhos — e se ela sai do retrato dos royalties, da comissão e dos pontos, e o que fica (a comissão da indicação, o
  cupom do sorteio e o do checkout; o passo 11) — pelas consultas de cada um contra a venda cancelada, e nunca chamando uma
  rotina de royalties (as do Asaas cobram franqueados de verdade);
- **a volta (as duas vezes):** a de "A volta", inteira, de dentro da manutenção, até o login do código antigo — **o tempo
  dela decide o limite das 5h** (a linha do tempo), somado à espera das rotinas, que o ensaio não tem;
- **o que ele não mede:** o cron (e por isso a espera das rotinas na volta), os provedores, o aplicativo de verdade, o
  disco e o PHP da produção, as três réplicas, e o backup da produção — a restauração do dump da rotina, que a sua 204 (a)
  pede ensaiada antes da janela, com o roteiro das três réplicas, se ensaia à parte, numa máquina que não é a produção (o
  passo 3; o item 8 de "O que falta").

---

## O que a etapa 2 herda, e o que muda nela

- **O passo 0 já aplicado.** Na etapa 2 o laço roda de novo e vira conferência: as migrações são idempotentes, e cada uma
  diz que já existe. O portão do passo 0 continua comparando com o dump de 16/08 (`3b1f05c`) até o passo 1 da etapa 2
  refazer o `mysql.sql` da produção: o cabeçalho dele manda trocar a base nesse dia, e as linhas já aplicadas podem sair.
- **O código novo já no ar.** O 6 de "O que hoje bloqueia" (o código do **D349**) e o deploy que os passos 3 e 9 pressupõem
  são conferidos, e não refeitos. O passo 7 guarda, pela primeira vez, um `local.php` que existe — o do MySQL que esta
  etapa criou —, e desde 08/11/2026 (o **D329**) não o troca mais pelo dist do PostgreSQL, o modelo de desenvolvimento:
  troca só o bloco `doctrine` dele (`scripts/trocar-doctrine-do-local-php.php`), e o `verificar-local-php.php` roda antes do 8,
  com a saída 1 parando.
- **O crontab já na forma nova** (a sua 194 (a)), **sem `MAILTO` e com o aviso no log** (a sua 128 (b), que vale também
  para a etapa 2: o `MAILTO` do passo 9 e o teste do e-mail do 8 de "O que hoje bloqueia" deixaram de servir lá, e a rodada
  do item 4 de "O que falta", em 07/11/2026, mudou o passo 9, o 8 e o ensaio deles para as duas etapas). O que o passo 1 da etapa 2 guarda já está
  em `Bearer`, com `-fsS` e `-m`: o primeiro bloco do passo 9 não muda linha nenhuma, e a conferência passa. Se esta etapa
  foi pelo recuo do passo 12 (a sua 193 (b)), o que o passo 1 da etapa 2 guarda é o de antes, sem as quatro linhas que
  saíram, e o passo 9 de lá o troca como estiver escrito. **Não rotacione o token de novo na etapa 2:** o
  `awk` só troca o `?token=`, as linhas em `Bearer` ficariam com o valor velho, a conferência passaria (ela confere a
  forma) e as rotinas dariam 401 — só a conta dos 10 minutos acusaria. O passo 7 já o diz: "Se a linha 9 já rodou antes
  da janela, o valor dela". E o `scripts/ensaiar-crontab-do-passo-9.sh` só ensaia a passagem da forma antiga para a
  nova: a da forma nova para ela mesma não está ensaiada.
- **As tabelas e as colunas novas cheias.** A frase do passo 4 — "Em produção elas estão vazias ou no padrão até o corte
  [...] e por isso nada se perderia desta vez" — deixa de valer: o código novo terá gravado `autorizado_por`, os
  `hash_anterior*`, a `usuarios_horarios`, os motivos e as alíquotas. Regerar o `migrar-dados.sh` no passo 4 já é a regra
  (**D379**), e passa a separar a carga certa da perda calada (o `COPY 0` do **D462**). A primeira prova do passo 5 (a
  contagem) pega a tabela que chegar vazia; **uma coluna de texto deixada de fora do `SELECT` não é pega por nenhuma das
  seis** — a soma é só das numéricas.
- **A semente do 5b acrescenta zero:** ela só põe linha em quem não tem nenhuma, e a carga traz a tabela cheia. Envelhecem
  as frases "nasce vazia, e a semente a preenche" (o 5b) e "a vigência do horário volta vazia" (o ponto de não retorno).
- **A volta da etapa 2 cai nesta etapa** — o código novo sobre o MySQL, já provado na produção —, e a semente da volta
  acrescenta zero.
- **As duplicatas podem crescer** no MySQL, que não tem os índices únicos: a limpeza do passo 3 roda de novo (é
  idempotente), e os 2.416 valores voltam a ser medidos na réplica.
- **Do passo 3, só o Líder rodou** (a sua 192 (a)): na etapa 2 ele roda de novo sem mudar nada, e o resto roda lá pela
  primeira vez — o "Vendas caixa" (a sua 122 (a)) e o dicionário de CEP (a sua 10 (b)) entre eles. As linhas ficam no passo
  3 do procedimento, que o portão dele lê.
- **As migrações das entregas entre as etapas** (a sua 200 (c)) vão à produção pelo roteiro de publicar, antes do código
  que as usa, e cada uma entra no passo 0 do procedimento como as doze (o portão exige): na etapa 2, o laço as confere com
  as outras.
- **O índice da grade, já aplicado** — ele não é entrega: entra no passo 0 desta janela (a sua 126 (a) e a sua 196 (b); o
  passo 4). O gerador do PostgreSQL já amplia o `FK_AGENDAS_colaborador` pelo nome (o mapa dos índices que ganham uma
  coluna, em `scripts/gerar-ddl-postgres.php`, o **D189**), e desde 07/11/2026 o `mysql.sql` diz o mesmo nome com as duas
  colunas (a fatia 114): o `postgres/schema.sql` regerado ficou igual nesse índice.
- **As limpezas de dentro do MySQL já estão no crontab** (a sua 195 (a)): a etapa 2 não precisa traduzir os eventos do
  **D26** para o PostgreSQL, que não tem agendador; o `limpar_push_app_24h` fica desligado no MySQL.
- **A cópia antiga das senhas, anulada** (a sua 202 (b)): a carga já a deixava para trás (o gerador tira a
  `usuarios.senha_php83`).
- **O que precisa ser acertado no procedimento antes de a etapa 2 rodar** — não mexido hoje, porque a decisão foi guardá-lo
  inteiro e quatro portões leem a estrutura dele:
  - **o nome do banco:** o passo 3 roda `mysql franquiabv`, e o de produção é o `franquia_producao` (o laço do passo 0;
    `ambiente-homologacao.md`). O portão do passo 3 exige o `mysql franquiabv` no texto: os dois mudam juntos;
  - **a versão:** "o 8.0.46 de produção" (o passo 0 e os cabeçalhos de duas migrações), contra o 8.4.7 da coleta de 19/08
    em `operacao-atual.md` e `ambiente-homologacao.md` — que também registram o 8.0.46 do dump de 16/08, tirado em
    `localhost`: a leitura 1 decide;
  - **as contagens:** "as dez" no ensaio geral (o laço tem 12). E o "São 25 entradas" do passo 1, que hoje está certo —
    a produção, congelada, ainda chama as quatro rotinas que saíram do código, e é o primeiro bloco do passo 9 que as tira
    (o parágrafo "O `crontab` só roda com a conferência batendo", no passo 9 do procedimento) —, deixa de valer depois desta etapa: o que o passo 1 da etapa 2 guardar terá 24
    linhas de `curl` (as 21, a dos convênios e as duas limpezas da sua 195 (a); eram 26 até 25/10/2026, quando a das 03:10 saiu do código, e 25 até 04/11/2026, quando a da Evasão das 04:10 saiu), pelos dois caminhos do passo 12;
  - **o `pgrep` do passo 1** não vê rotina nenhuma: elas rodam no php-fpm ("1 e 2", acima), e a frase "Só depois de o
    cron estar parado e nenhuma rotina em curso" pede o sinal do `SHOW PROCESSLIST`;
  - **a passagem da manutenção** (o passo 2 de lá não diz como a equipe e o servidor passam), e o teste à mão do passo 8,
    que lança uma venda de verdade sem dizer o que fazer com ela;
  - **os comandos de laboratório em passo de produção:** o 7b, o 7c e a volta atrás usam `docker compose exec` e
    `-uroot -proot`, e não se sabe se a produção roda em Docker (`operacao-atual.md`, "Deploy");
  - **a resposta 127** (as limpezas de dentro do MySQL viram rotinas do crontab, **D26**) não está no procedimento — está
    nesta etapa, pela sua 195 (a);
  - **a resposta 128** (sem e-mail, o aviso no log): **feita em 07/11/2026** (a fatia 113) — o 8 de "O que hoje bloqueia"
    testa o log do sistema, e o passo 9 instala sem endereço, com o `logger` em cada linha;
  - **a tabela do que o passo 3 escreve e sobrevive à volta** não traz as duplicatas de `dashboard_colaboradores` e de
    `royalties` que a limpeza apaga, a view `v46` que ela derruba, nem o "Vendas caixa";
  - **o php-fpm do item 4 de "Preparação"** diz "aplicar é do dono, no servidor, antes da janela": nesta etapa ele entra
    na janela (a sua 205 (a); os passos 8 e 9), e na etapa 2 é conferência — o mesmo vale para a memória do MySQL, se a
    leitura 4 a tiver ajustado;
  - **o backup**: o procedimento não tem passo de backup; a forma desta etapa (a sua 204 (a), o passo 3) é a candidata
    para a etapa 2, que também escreve antes do dump dela (o passo 3 de lá).

---

## O que falta medir

Tudo só de leitura, e nada medido daqui:

1. as vinte e duas leituras de "O que se lê na produção";
2. o tempo dos três `ALTER` que copiam a tabela, no tamanho da produção, e o estado e o atraso das três réplicas enquanto
   eles rodam (o ensaio da sua 199 (a), no banco descartável, dá o tempo; o estado e o atraso, só a produção: a leitura 21
   e os passos 4 e 13);
3. o tempo do dump e o da restauração (o dump da rotina, pela sua 204 (a), e os quatro fatos dela, a leitura 14; a restauração num primário com três réplicas, e
   o `GTID_PURGED`), e os privilégios da conta que roda o laço, o `ALTER EVENT` (o `SET_ANY_DEFINER`, o `SET_USER_ID`
   no 8.0, ou o `SUPER`: sem ele, a cláusula `DEFINER` do passo 12 recusa, e o evento fica ligado) e as duas linhas do passo 6;
4. a mais longa das rotinas que ficaram (o teto do php-fpm, **D67**: a de 21 minutos saiu em 13/10/2026);
5. o espaço livre no disco do diretório de dados para os três `ALTER` que copiam, somado ao dump (a leitura 13);
6. se uma sessão aberta no código antigo segue no novo: o código trata a sessão entre versões novas, não a do código de
   julho; o seguro é contar que todos entram de novo;
7. como a página de manutenção deixa passar a equipe e o próprio servidor, e por onde o servidor chega ao domínio (o
   `/etc/hosts` ou o IP público);
8. o código novo diante das datas de ano 0 da produção: a cópia não as tem, porque o anonimizador reescreve o nascimento;
9. o usuário do pool do php-fpm, se ele tem página de status, o `process_control_timeout`, e se a aplicação usa conexão
   persistente ao MySQL (sem ela, a conexão em `Sleep` do passo 2 é um pedido em curso);
10. se a venda de teste cancelada sai também do retrato dos royalties, da comissão e dos pontos (no ensaio, a sua 203 (a));
11. o tamanho do pacote da release e o tempo da cópia para o servidor, no começo da janela (a sua 201 (a));
12. o tempo dos dois `ALTER` maiores, que o ensaio no BVIP só mede no banco descartável, sem as duas colunas (a sua 199
    (a));
13. o tempo da volta inteira, de dentro da manutenção — o que decide se o limite das 5h basta (a linha do tempo) —, mais a
    espera das rotinas que o crontab novo disparou, se a volta vier depois do passo 12 (o ensaio não a tem);
14. a memória do MySQL e a da máquina (a leitura 4; se for a de fábrica, o valor que a sua 205 (a) põe na janela);
15. ~~na cópia, se algum atendimento do aplicativo em 2026 tem o serviço de outra unidade que não a do colaborador~~ —
    **medido em 14/10/2026** (`franquiabv_dev`, só contagens; o atendimento é a primeira linha do bloco, `id_pai` nulo,
    `origem = 'app'`, pela data do horário): **0 de 71.402** em 2026; 341 de 969.426 em todos os anos (192 em 2018, 113
    em 2019, 14 em 2020, 2 em 2021 e 20 em 2025). O número serviu à pergunta das duas guardas, **decidida em 15/10/2026:
    a 208 (a)** — as duas ficam, e, com zero, nada volta ao dono ("As dez rotas do aplicativo instalado").

---

## As decisões do dono

**As quinze do corte em duas etapas** (CORTE-EM-DUAS-ETAPAS) foram respondidas na página do lote em 14/10/2026, e estão
aplicadas neste documento. O texto de cada opção escolhida está no arquivo do lote; aqui, em poucas palavras:

| | a letra | o que ela diz | onde entrou |
|---|---|---|---|
| 189 | (b) | as dez rotas do aplicativo abertas ao aplicativo de hoje, como estão na produção, e fechadas quando o aplicativo novo estiver em campo; código antes da janela | "As dez rotas do aplicativo instalado"; o item 1 de "pronto" e de "O que falta"; o passo 11; a volta — **feito em código em 14/10/2026** (o interruptor `ROTAS_DO_APLICATIVO_INSTALADO`, em `abertas`); as duas guardas que não leem a sessão **ficam** (a 208 (a)), e o colaborador da unidade inativa no V2 e no V3 **é recusado** (a 226 (b), feito em código em 15/10/2026) |
| 190 | (a) | uma noite de dia útil, fora dos dias 15 e 16 e da virada do mês, das 00:05 até antes das 05:25, com o limite das 5h; testa o dono ou quem ele indicar, com um usuário de cada papel, por uma manutenção que deixa passar só a equipe e o servidor; 48 horas de acompanhamento, e de novo na fatura do dia 16 | os passos 1, 2, 11 e 13; "A linha do tempo da janela"; "Depois da janela"; a leitura 12; o item 6 de "pronto"; o item 10 de "O que falta" — o retrato da véspera, se a janela ou a volta passar das 05:25, **à mão no mesmo dia** (a 209 (a)) |
| 191 | (a) | o passo 0 dentro da janela, com o sistema parado, depois do backup | os passos 3 e 4 |
| 192 | (a) | do passo 3, só o Líder entra nesta etapa, na janela, com a linha que o desfaz escrita e ensaiada; o resto na etapa 2 | "O que esta etapa NÃO faz"; o passo 6; a volta; o item 7 de "O que falta"; "O que a etapa 2 herda" |
| 193 | (b) | o `cron.token` na janela, com o crontab novo — e, se ele não puder entrar, o de hoje; as outras oito depois de o dono declarar o fim da volta, e até lá os valores de hoje | "O `local.php` desta etapa"; o passo 12 e o recuo, que é a reserva da janela; a volta; o item 5 de "O que falta" — depois de um recuo, **o crontab novo na noite seguinte** (a 207 (a)) |
| 194 | (a) | o crontab novo nesta etapa, com o domínio raiz, `franquiabv.com.br` — e sem endereço de e-mail: vale a 128 (b), que a pergunta 194 esqueceu (ela ainda dizia "Falta o e-mail que recebe o aviso"), e nenhum endereço veio | o passo 12; o item 8 de "pronto" (a janela espera o crontab novo); o item 4 de "O que falta"; "O que a etapa 2 herda" |
| 195 | (a) | as duas limpezas de dentro do MySQL viram rotinas do crontab desta etapa, e a de hora em hora é desligada na mesma janela; a volta a religa | a leitura 5; o passo 12; "Depois da janela"; a volta; o item 3 de "O que falta" |
| 196 | (b) | o índice da agenda (o D189) entra no passo 0 desta etapa, além de ler a memória do MySQL | a leitura 4; o passo 4; o item 2 de "O que falta" — e a memória de fábrica **se ajusta na janela** (a 205 (a)) |
| 197 | sem opção | *"O Backup já é feito por rotina, de hora em hora um dump local (24 horas de rotação), outros três vps usam a replicação do mysql em tempo real. além do backup diário da Hostringer"* | as leituras 11, 14 e 21; o passo 3; o item 8 de "O que falta" — a forma do backup da janela é **o dump da rotina, disparado à mão** (a 204 (a)) |
| 198 | (a) | a volta vale até o dono declarar o fim, por escrito, depois das 48 horas e da primeira conversa do código novo com o Asaas; até lá, nada irreversível | a volta ("Até quando se volta"); a regra das entregas, em "Depois da janela" |
| 199 | (a) | o ensaio no BVIP: a troca e a volta, duas vezes, cronometradas, com o código antigo lá só durante o ensaio; o passo 0 num banco descartável, sem as duas colunas; a base do teste não muda | "O ensaio da etapa 1" (duas vezes a troca e a volta; uma vez o resto); o item 9 de "pronto"; o item 11 de "O que falta" |
| 200 | (c) | a produção recebe as entregas normalmente desde o dia seguinte | a regra das entregas, em "Depois da janela"; o item 9 de "O que falta" — quando e como se publicam: **à noite, como a janela** (a 206 (a)) |
| 201 | (a) | o pacote e o `local.php` chegam ao servidor no começo da janela, numa pasta ao lado que o site não serve; nada muda lá antes | "A release, montada fora da produção"; o item 4 de "pronto"; os passos 1 e 7 — o php-fpm, que o procedimento punha antes da janela, **entra na janela** (a 205 (a)) |
| 202 | (b) | as duas formas do `migrar-senhas` negadas no nginx, na janela e de novo na volta, **e** a cópia antiga das senhas apagada do banco na janela, depois do backup | os passos 3, 6 e 8; a volta; o item 6 de "O que falta" — a cópia é a `senha_php83`; a `salt` do D78, que a opção cita, fica (o passo 6) |
| 203 | (a) | a venda de teste no cadastro de quem testa, numa unidade combinada, cancelada pela tela antes de as rotinas voltarem, com o motivo "teste do corte"; o aviso ao aparelho de quem testa | o passo 11; o ensaio; o item 12 de "O que falta" |

**As respostas anteriores que valem para o corte**, e que esta etapa respeita:

| | a letra, a data | o que ela diz | onde entrou |
|---|---|---|---|
| 5 | (b), 06/10/2026 | o crontab em `curl -fsS`, com o token no cabeçalho — e o `MAILTO`, que a 128 trocou pelo log | o passo 12 |
| 7 | (a), 06/10/2026 | a suíte de caracterização fora da janela | "O que esta etapa NÃO faz" (nunca a suíte contra a produção) |
| 10 | (b), 06/10/2026 | o dicionário de CEP esvaziado no passo 3 | fica na etapa 2, pela 192 (a) |
| 11 | (b), 06/10/2026 | o Líder na janela, com o sistema parado | o passo 6 |
| 31 | (a), 07/10/2026 | um endereço de e-mail para o aviso das rotinas — **mudada pela 128** | — |
| 50 | (a), 08/10/2026 | o índice de `id_pai` pelo passo 0 | o passo 4 (já no laço) |
| 62 | (b), 09/10/2026 | o índice de `id_pai` nas duas cópias locais; a base do teste sem ele | "O ensaio da etapa 1" |
| 83 | (a), 12/10/2026 | o que só acrescenta entra nas duas cópias locais, com a sua migração no passo 0; a base do teste, na próxima cópia exportada | o passo 4; o item 2 de "O que falta" |
| 106 | (a), 12/10/2026 | as três rotas antigas que marcam e desmarcam recortadas pela unidade da sessão | "As dez rotas do aplicativo instalado": **saiu junto enquanto as dez estiverem abertas** (14/10/2026: sem sessão, o recorte recusaria toda marcação), e volta quando elas fecharem, com a constante; a verificação dela que não lê a sessão (o serviço da unidade do colaborador) **fica** (a 208 (a)) |
| 122 | (a), 12/10/2026 | o "Vendas caixa" sai da lista no passo 3 | fica na etapa 2, pela 192 (a) |
| 123 | (a), 12/10/2026 | a pontuação de todos recalculada pelo histórico, uma vez, no corte | o passo 7 (`scripts/recalcular-pontuacao-no-corte.sql`), depois da troca do código; o item 18 de "O que falta" (a fatia 106) |
| 126 | (a), 12/10/2026 | o índice da grade da agenda pelo passo 0, e o mesmo no `mysql.sql` | o passo 4; o item 2 de "O que falta" |
| 127 | (a), 12/10/2026 | as duas limpezas viram rotinas do crontab, com os prazos de hoje (24 horas e 60 dias) | o passo 12; o item 3 de "O que falta" (a da fila, ao menos uma vez antes do envio das 10:00) |
| 128 | (b), 12/10/2026 | sem e-mail: o aviso de erro das rotinas vai ao log do sistema | a leitura 20; o passo 12; "Depois da janela"; o item 4 de "O que falta" |
| 158 | (a), 12/10/2026 | a rota antiga do aplicativo que desmarca recusa a linha do bloqueio, sem cliente; "o aplicativo não muda" | "As dez rotas do aplicativo instalado": **fica** com a reabertura (a 208 (a)) |
| 182 | (a), 14/10/2026 | os três privilégios sem tela ficam na lista, sem efeito | "O que esta etapa NÃO faz": nada a apagar |
| 184 | (a), 14/10/2026 | a limpeza de 24 horas poupa a mensagem que falhou, que sai em 7 dias | o passo 12; o item 3 de "O que falta" |
| 185 | (a), 14/10/2026 | os 7 dias contam do pedido | o passo 12 (já no código) |
| 186 | (c), 14/10/2026 | o parabéns e o "sentimos sua falta": 100 de cada por dia, como está | o passo 12 e "Depois da janela": a linha não muda, e a limpeza da fila roda antes do envio das 10:00, como o evento de hoje |
| 230 | (a), 15/10/2026 | antes do recálculo da 123, a linha de saldo zerada na unidade de cada lançamento de pontos `ativa` que não a tem | o mesmo roteiro do passo 7, antes do recálculo; o item 18 (a fatia 106) |

**As seis perguntas que voltaram ao lote** (CORTE-ETAPA-1-ESCOLHAS, 14/10/2026) — **todas decididas em 15/10/2026** (a tabela de baixo):

| | a pergunta | por que voltou |
|---|---|---|
| 1 | o backup da janela | a 197 veio sem opção marcada: o que existe não é, sozinho, o backup da janela que a 191 (a) e a 202 (b) pedem (a rotação de 24 horas, as réplicas que recebem tudo, o diário que não se sabe), a rotina de hora em hora não pode disparar no passo 4, e a forma do passo 3 — com ou sem o ensaio da restauração — é do dono — **decidido em 15/10/2026: a 204 (a)** |
| 2 | os ajustes no servidor: a memória do MySQL, se a leitura der a de fábrica, e os três do php-fpm | a 196 (b) pede a leitura e não diz o que fazer com o número; o procedimento pôs o php-fpm "antes da janela", e a 201 (a) diz "Nada muda no servidor antes da janela"; os dois são mudança no servidor, do dono — **decidido em 15/10/2026: a 205 (a)** |
| 3 | as entregas depois da janela: quando e como se publicam | a 200 (c) abre as entregas desde o dia seguinte; a regra que a faz caber na 198 (a) está escrita, e a hora e o jeito da troca — com o site no ar ou com a manutenção — são do dono — **decidido em 15/10/2026: a 206 (a)** |
| 4 | se o crontab novo não entrar na janela: o recuo, e quando o novo entra depois | a 193 (b) deu o recuo para a falta do e-mail, que a 128 (b) tirou; ele segue como reserva da janela, e nenhuma resposta diz quando o crontab novo entra depois de um recuo, nem quanto tempo as rotinas ficam sem aviso — **decidido em 15/10/2026: a 207 (a)** |
| 5 | as dez rotas reabertas: as duas guardas que não leem a sessão | a 189 (b) diz "como estão hoje na produção", e a produção de hoje não tem o serviço da unidade do colaborador (a verificação da 106) nem o bloqueio do desmarcar (a 158). Medido para ela em 14/10/2026: 0 dos 71.402 atendimentos de 2026 pelo aplicativo, na cópia, têm o serviço de outra unidade; as duas ficam até a resposta — **decidido em 15/10/2026: a 208 (a)** |
| 6 | o retrato dos royalties da véspera, se a janela ou a volta passar das 05:25 | a rotina das 05:25 só recalcula ontem e hoje: pulada, a véspera não se recupera, e a fatura do dia 16 sai a menos; nenhuma resposta diz o que fazer — **decidido em 15/10/2026: a 209 (a)** |

**E uma da verificação das dez rotas** (AS-DEZ-ROTAS-ABERTAS, 15/10/2026), no fim do mesmo arquivo — **decidida no mesmo dia** (a 226):

| | a pergunta | por que voltou |
|---|---|---|
| 7 | o V2 e o V3 abertos e o colaborador da unidade inativa: recusam, ou ficam como a produção | a 179 (b) recusou o colaborador de unidade inativa na agenda e na v4, e deixou o V2 e o V3 de fora porque eles exigiam a sessão; a 189 (b) os abriu sem sessão, e "nem maior nem menor" deixa a porta como a produção. Na cópia, 99 colaboradores ativos em 24 unidades inativas, nenhum atendimento pelo aplicativo com eles nos 90 dias antes da cópia; até a resposta, ficam como a produção — **decidido em 15/10/2026: a 226 (b)** |

**As sete respondidas em 15/10/2026** — as seis que voltaram ao lote em 14/10/2026 (CORTE-ETAPA-1-ESCOLHAS) e a da
verificação das dez rotas (AS-DEZ-ROTAS-ABERTAS) —, na página do lote: as perguntas 204 a 209, todas na opção (a), e a 226,
na (b). O texto de cada opção escolhida está no arquivo do lote; aqui, em poucas palavras:

| | a letra | o que ela diz | onde entrou |
|---|---|---|---|
| 204 | (a) | o backup da janela é o dump da rotina, disparado à mão, com o sistema parado e antes das migrações; a rotina de hora em hora parada com o cron no começo da janela, e religada no fim e na volta; o dump copiado para fora da rotação e do servidor, e guardado até o dono declarar o fim da volta; a restauração ensaiada antes, numa máquina que não é a produção, com o roteiro das três réplicas | o passo 3; as leituras 11, 13 e 14; os passos 1 e 12, e a volta; o item 10 de "pronto"; o item 8 de "O que falta"; "O ensaio da etapa 1" — **os quatro fatos que a pergunta pedia não vieram com a resposta**: o passo 3 se escreve com eles (a leitura 14, do dono) |
| 205 | (a) | os dois ajustes no servidor na janela: a memória do MySQL antes das migrações, se a leitura der a de fábrica, no valor que a memória da máquina permitir; o php-fpm junto com o vhost novo e o recarregar (o pool das rotinas é um arquivo do php-fpm e uma entrada a mais no vhost); a volta não desfaz nenhum dos dois | as leituras 4 e 7; os passos 4, 8 e 9; o item 7 de "pronto"; o item 14 de "O que falta"; a volta — **o teto curto do pool do site, que a volta não desfaria, foi ao lote** (o **D497**, da verificação), e o dono o decidiu no mesmo dia: a 228 (a), na tabela de baixo |
| 206 | (a) | as entregas depois da etapa 1 saem à noite, como a janela: depois das 00:05 e antes das rotinas da 01:10, com a manutenção, o cron parado e a espera das rotinas em curso | "Depois da janela" (as entregas, com a hora-limite: liberada até 01:10 menos o tempo da volta, ou volta-se à liberação anterior, como o limite das 5h da janela); o item 9 de "O que falta" |
| 207 | (a) | se o crontab novo não entrar na janela, o recuo, e o crontab novo na noite seguinte: depois das 00:05, o mesmo bloco do passo 12 e a conta dos 10 minutos, com o site no ar; até lá, um dia sem aviso | o passo 12 (o bloco da noite seguinte); o item 8 de "pronto"; o item 4 de "O que falta" |
| 208 | (a) | as duas guardas das dez rotas que não leem a sessão ficam; se a contagem na cópia desse mais que zero, o número voltaria ao dono — deu 0 de 71.402, e nada volta | "As dez rotas do aplicativo instalado"; o item 1 de "pronto" e de "O que falta"; a volta — nada mudou no código: elas já ficavam |
| 209 | (a) | se a janela ou a volta passar das 05:25, o retrato à mão, no mesmo dia: depois de liberar, o dono ou quem ele indicar dispara uma vez a rotina das 05:25, antes da meia-noite | "A linha do tempo da janela" (o bloco do retrato: pulado é o dia em que o crontab só voltou depois das 05:25, conferido pelo log do nginx; uma vez só, porque o código antigo não tem trava; e, numa volta na noite de 15 para 16, antes das 09:00 da fatura); o passo 13; "Depois da janela" |
| 226 | (b) | o V2 e o V3 abertos recusam já o colaborador da unidade inativa, com a resposta do id que não acha ninguém ("Informe o colaborador."), como a marcação nova (a 179 (b)) | "As dez rotas do aplicativo instalado"; o item 1 de "pronto" e de "O que falta"; a volta — **feito em código em 15/10/2026** (o `AplicativoController::semOColaboradorDaUnidadeInativa`, nos dois estados do interruptor; o **D482**) |

**E as duas da verificação de 15/10/2026** (as telas que faltam, TELAS-RESTANTES [corte]) — **respondidas no mesmo dia**,
na opção (a), a recomendada:

| | a letra | o que ela diz | onde entrou |
|---|---|---|---|
| 228 | (a) | o teto curto do pool do site só depois de o dono declarar o fim da volta; até lá, os dois pools com o teto das rotinas; depois da declaração, o teto curto entra numa entrega à noite, como as outras; a volta fica como a 205 (a), sem nada a desfazer | os passos 8 e 9; o item 7 de "pronto"; o item 14 de "O que falta"; "Depois da janela" (a entrega do teto curto); a volta e "Até quando se volta" — o **D497**, decidido |
| 229 | (a) | conferir antes do corte: a listagem das travas do servidor entra nas leituras, sem tocar em nada; se aparecer uma trava de marketing de outro usuário, o dono decide, antes de o código subir, se aquele envio volta a rodar ou fica parado | a leitura 22, com os três casos pelo modo da trava; o item 12 de "pronto" — a nota no **D499** |

**E as três das telas que faltam, de 04/11/2026** (TELAS-RESTANTES [relatorios] e [cadastro]; a fatia 76u, RESPOSTAS-294-A-300),
na opção recomendada:

| | a letra | o que ela diz | onde entrou |
|---|---|---|---|
| 294 | (a) | a Carteira de clientes com a conta exata e a espera de 1 a 2 s; o índice das vendas por cliente no passo 0 desta etapa, e não na migração final (muda o D308) | o passo 4; o item 15 de "O que falta" — o **D308** e o **D697** |
| 295 | (a) | o consumo dos clientes refeito uma vez no corte, e a rotina refazendo também quem teve venda cancelada | o passo 7; o item 16 de "O que falta"; "O que fica no banco" — o **D698** |
| 298 | (c) | as quatro permissões com o nome da tela, renomeadas no dia da virada, junto com o sistema novo; ninguém perde permissão | o passo 7; o item 17 de "O que falta"; a volta (o item 4 e "O que quebra a volta") — o **D701** |

**O que segue do dono, e não é escolha:** os quatro fatos do
backup (a leitura 14), o destino do dump fora do servidor, as leituras de "O que se lê na produção" (a 22 também), e o
que ele roda no servidor e no BVIP.

---

## Os portões, e por que este documento não está neles

Quatro portões leem o `procedimento-de-corte.md` pela estrutura dele: `scripts/conferir-passo-zero-do-corte.php` (as
linhas `| 0 |` e **exatamente um** laço contra `franquia_producao`), `scripts/ensaiar-sql-do-passo-3.php` (o texto do
passo 3, até o 4), `scripts/ensaiar-crontab-do-passo-9.sh` (os blocos do passo 9 e o do 8) e
`scripts/ensaiar-medir-chamadas-unidade-vendas.sh` (o `ssh` de "O que hoje bloqueia"). Escrever esta etapa dentro dele
deixaria o do passo 0 com saída 2 (um segundo laço da produção) e o do passo 3 vermelho, se o Líder ou o "Vendas caixa" saíssem do passo 3 para cá.

**Um portão lê este documento, desde 25/10/2026:** o `scripts/ensaiar-crontab-do-passo-9.sh` tira daqui a linha do
`grep -vE` do recuo do passo 12 e a roda sobre o crontab inventado. Ele a procura dentro do título `### 12. `, até o
próximo `### `, e exige **uma e só uma** linha inteira na forma `grep -vE ... /root/crontab-antes-do-corte.txt >
/root/crontab-recuo.txt`. Renomear o título do passo 12, quebrar essa linha com `\`, mudar um dos dois caminhos ou escrever
uma segunda linha igual no passo deixa o ensaio com saída 2 -- e com ele o `scripts/conferir-tudo.sh` e o CI. Quem mudar o
recuo muda o ensaio junto.

Por isso este documento **aponta** para o laço do passo 0, para o passo 3, para os blocos do passo 9 e para o 8, e não os
copia: o que ele manda rodar é o texto que os portões conferem. O resto — a release, as pastas de upload, a troca, o recuo
do crontab (menos a linha do `grep -vE`, acima: a conferência do token e a instalação), a manutenção, a volta — fica sem
portão até ser ensaiado. **Se um dia alguém copiar o laço do passo 0 para cá, ele fica sem portão**: o
`conferir-passo-zero-do-corte.php` pode passar a recusar um laço contra `franquia_producao` neste arquivo, e é mudança
pequena, **não feita** em 14/10/2026. O `scripts/conferir-referencias-docs.php` lê este documento como todo `docs/*.md`:
os caminhos citados existem — e por isso o que ainda não existe (a migração do índice da grade, a do índice das vendas por
cliente, o roteiro do consumo, a página de
manutenção, o roteiro do ensaio) está descrito, e não citado por um caminho. O roteiro de publicar existe desde 08/11/2026
(`scripts/publicar-producao.sh`, com o ensaio local `scripts/ensaiar-publicar-producao.sh` no `conferir-tudo.sh`); a troca
e a volta que ele faz têm esse ensaio, e não o da produção.

**Três das decisões aplicadas aqui mudam texto que os portões leem**, e mudam junto com eles, na rodada que as fizer: a
migração do índice da grade (a 126 e a 196) e a do índice das vendas por cliente (a 294 (a)) entram na tabela e no laço do passo 0
do procedimento e no laço do ensaio geral (o `conferir-passo-zero-do-corte.php`); e o crontab sem o `MAILTO` (a 128 e a 194) mudou os dois blocos do passo 9 e o do 8, com o
`ensaiar-crontab-do-passo-9.sh` junto (07/11/2026, a fatia 113). As duas linhas do passo 6 — o inverso do Líder e a cópia das senhas — e o
`ALTER EVENT` do passo 12 ficam sem portão até o ensaio.
