From ab126d13dbb4ee55c31b4ff209a4abe67494d619c40f337544711fca35fe630f Mon Sep 17 00:00:00 2001 From: victor Date: Thu, 27 Aug 2026 19:57:33 -0300 Subject: [PATCH] =?UTF-8?q?Atualiza=C3=A7=C3=A3o=20de=20Read.me?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 333 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 253 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 9036554..b006dd7 100644 --- a/README.md +++ b/README.md @@ -3072,118 +3072,291 @@ caminho do servidor e o trace inteiro — para **qualquer pessoa** que provocass ---
-🕐 Relógio, restart automático e migrations no boot +🛠️ 27/08/2026 — Deploy: relógio, arquivos órfãos e tudo que caiu junto -## O problema relatado +Um relato só, porque foi **uma corrente**: um problema descoberto expunha o +seguinte. Vale mais como mapa de diagnóstico do que como changelog. -> "toda vez que faço o deploy ou reinicio o Docker a data e hora ficam erradas." - -São **dois problemas diferentes** que se confundem porque aparecem no mesmo lugar: - -| Sintoma | Causa | Onde se resolve | +| Sintoma na tela | Causa real | Onde se resolveu | |---|---|---| -| Hora **sempre 3h adiantada**, todo restart | O container subia em **UTC** | `Dockerfile` (fuso) | -| Hora **derivando / errada depois de reboot** | Relógio do **host** sem sincronia | `deploy/ntp-seguro.sh` (NTS) | +| Hora sempre 3h adiantada | Container subia em **UTC** | `Dockerfile` (TZ + `/etc/localtime`) | +| Hora errada depois de reboot | Relógio do **host** sem sincronia | `deploy/ntp-seguro.sh` (chrony + NTS) | +| `env file .env not found` | `.env` não é versionado e a pasta foi recriada | recriar no servidor + backup fora da árvore | +| `Couldn't find Active Storage configuration` | `config/storage.yml` nunca esteve no git | arquivo versionado | +| `port is already allocated` | Porta 3001 vivia como alteração **local** no compose | `${PORTA_APP:-3001}` | +| `Blocked hosts: 100.75.222.23:3001` | `config.hosts` ligado só com o domínio | `HOSTS_PERMITIDOS` | +| `Translation missing … devise.failure` | `fallbacks = true` caía no próprio pt-BR | `fallbacks = [:en]` + traduções | +| 422 "recusada por verificação de segurança" | `assume_ssl` fixo x acesso por IP em HTTP | gate do `FORCE_SSL` | +| `URI::InvalidURIError: … SEU_IP` | `DATABASE_URL` com o valor de exemplo | falha rápida no boot | -### 1. Fuso do container (o "sempre 3h à frente") +## 1. Relógio: fuso é do container, hora é do host -A imagem `ruby:3.2.2-slim` sobe em UTC. O Rails até mostrava a hora certa -(`config.time_zone = "America/Sao_Paulo"`), mas **tudo que era do sistema -operacional continuava adiantado** — e o pior caso não era visual: +A imagem `ruby:3.2.2-slim` sobe em UTC. O Rails mostrava a hora certa +(`config.time_zone`), mas **tudo do sistema operacional** ficava adiantado — e o +pior caso não era visual: -> **O cron rodava no horário errado.** `every "*/30 8-18"` no `config/schedule.rb` -> executava das **05h às 15h** de Brasília, não das 08h às 18h. +> **O cron rodava no horário errado.** `every "*/30 8-18"` executava das **05h às +> 15h** de Brasília. -Agora o `Dockerfile` fixa `TZ=America/Sao_Paulo` **e** o symlink de -`/etc/localtime` — os dois, porque o cron do Debian lê o arquivo, não a -variável. O mesmo foi feito no `whatsapp/Dockerfile` (Alpine precisa do pacote -`tzdata`, que não vem na imagem). Dá para ajustar sem rebuild pelo `TZ` do `.env`. +O `Dockerfile` agora fixa `TZ=America/Sao_Paulo` **e** o symlink de +`/etc/localtime` — os dois, porque o cron do Debian lê o arquivo, não a variável. +Mesmo tratamento no `whatsapp/Dockerfile` (Alpine precisa do pacote `tzdata`). -### 2. Hora do host — NTP autenticado (NTS) - -**Não dá para sincronizar o relógio de dentro do container**: ele lê o clock do -kernel do host. Rodar NTP lá dentro exigiria `CAP_SYS_TIME` e mudaria a hora do -servidor inteiro a partir de um processo da aplicação. - -No servidor, uma vez: +**A hora em si não dá para sincronizar de dentro do container**: ele lê o clock do +kernel do host, e rodar NTP lá dentro exigiria `CAP_SYS_TIME`. Por isso +`deploy/ntp-seguro.sh`, que roda **uma vez no servidor** e instala **chrony com +NTS** (RFC 8915): troca de chaves por TLS (TCP 4460) e pacotes NTP assinados. +NTP comum é UDP sem autenticação, e hora errada aqui muda **o dia da +consolidação, o recorte do período financeiro e a validade da sessão**. O script +usa 3 fontes independentes, **desliga as não autenticadas** (deixar o `pool` +padrão anularia o ganho), desativa o `systemd-timesyncd` e liga o `rtcsync`. ```bash sudo bash deploy/ntp-seguro.sh +chronyc tracking # 'System time' na casa dos milissegundos +chronyc -N authdata # NTS ativo em cada fonte ``` -O script instala e configura o **chrony com NTS** (RFC 8915): a troca de chaves -é por TLS (TCP 4460) e os pacotes NTP vêm assinados, então uma resposta forjada -no caminho é descartada. NTP comum é UDP sem autenticação — e hora errada aqui -não é detalhe: ela muda **o dia da consolidação, o recorte do período financeiro -e a validade da sessão**. +## 2. Restart automático e migrations -O script ainda: usa **3 fontes independentes** (Cloudflare, Netnod, PTB), -**desliga as fontes não autenticadas** (deixar o `pool` padrão anularia o ganho), -desativa o `systemd-timesyncd` (não fala NTS e brigaria pelo relógio), liga o -`rtcsync` (é o relógio de hardware que dá a hora no boot) e mostra a -conferência no fim. É idempotente e guarda `.bak` da config. +`restart: unless-stopped` no serviço `app`. Escolhido em vez de `always` porque +`always` reergue o container até depois de um `stop` deliberado, tirando do +operador a chance de deixar o sistema fora do ar de propósito. -Conferir depois, a qualquer momento: +O boot saiu do `command:` de uma linha e virou **`bin/docker-boot`**: -```bash -chronyc tracking # 'System time' deve ficar na casa dos milissegundos -chronyc -N authdata # NTS ativo em cada fonte (Cook > 0) +1. **relógio** — imprime hora e fuso (se aparecer UTC, o rebuild não pegou); +2. **pid órfão**; 3. **gems** (o volume `bundle_cache` sombreia os da imagem); +4. **migrations** — `db:prepare`, com **espera pelo banco**; +5. **cron** (best-effort); 6. **Puma**. + +A migration é automática a todo restart. A espera do passo 4 existe porque o +PostgreSQL é externo ao compose: num reboot o Rails pode subir antes de o banco +aceitar conexão e, com `unless-stopped`, isso viraria **loop de reinício** +parecendo erro de migration. + +**Falha rápida:** `DATABASE_URL` vazia ou com o valor de exemplo não é "banco fora +do ar" — nenhuma espera resolve. O boot para na primeira tentativa dizendo o que +preencher, **com a credencial mascarada no log**. Este projeto **não tem +`config/database.yml`**: a conexão inteira sai dessa variável, e ninguém lê +`DB_HOST`/`DB_NAME`/`DB_USER`/`DB_PASSWORD`. + +## 3. Arquivos que o git não levava — e por isso sumiram juntos + +A pasta do deploy foi recriada e levou embora todo arquivo **não versionado**. Dois +apareceram: + +- **`.env`** — está no `.gitignore` por conter senha, e assim deve continuar. + Guarde uma cópia fora da árvore do projeto e prefira `git pull`/`reset --hard` + na própria pasta a recriá-la (arquivo ignorado sobrevive ao pull). +- **`config/storage.yml`** — não tinha motivo para estar de fora. O app faz + `require "rails/all"`, então o Active Storage sempre carrega e os + `config/environments/*.rb` definem `active_storage.service = :local`: com um + serviço definido, o Rails **exige** o arquivo no `initialize!` e o servidor não + sobe. **Agora é versionado.** + +## 4. Configuração do ambiente saiu do `.env` e entrou no compose + +A porta do teste (**3001**, porque a 3000 está ocupada por outro stack) vivia como +alteração **local** no `docker-compose.yml` do servidor — e um `git reset --hard` +a levou junto, derrubando o ambiente com o proxy apontando para uma porta sem +ninguém escutando. Agora os valores **não secretos** têm padrão no compose: + +```yaml +ports: + - "${PORTA_APP:-3001}:3000" +environment: + TZ: ${TZ:-America/Sao_Paulo} + APP_HOST: ${APP_HOST:-teste.reemtransportes.com.br} + APP_NAME: "${APP_NAME:-Reem Logística}" + WHATSAPP_URL: ${WHATSAPP_URL:-http://whatsapp:3001} + HOSTS_PERMITIDOS: ${HOSTS_PERMITIDOS:-100.75.222.23} ``` -> **Firewall:** precisa de saída em **UDP 123** e **TCP 4460**. Sem a 4460 o NTS -> não fecha e o chrony fica sem fonte. +`${VAR:-padrão}`: **o `.env` continua mandando**; o padrão só entra quando a +variável falta. Segredos (`DB_*`, `SECRET_KEY_BASE`, `SIMPLIROUTE_TOKEN`, +`WHATSAPP_TOKEN`, `SMTP_*`, `NOTIFICACAO_SECRET`) **nunca** vão para o yaml. -## Restart automático + migrations +> ⚠️ **`APP_HOST` é só o host, sem `https://`.** O código monta os links como +> `"https://#{APP_HOST}/motorista"` (`consolidacao_mailer.rb`, +> `notificacao_service.rb`, `gatilhos.rb`): com o esquema no valor sai +> `https://https://…` e todo link de e-mail e WhatsApp quebra. -`docker-compose.yml`, serviço `app`: **`restart: unless-stopped`**. Sobe sozinho -depois de queda do processo e de reboot do servidor, e só fica parado se alguém -der `docker compose stop/down`. (`always` foi descartado: ele reergue o container -até depois de um `stop` deliberado, tirando do operador a chance de deixar o -sistema fora do ar de propósito.) +## 5. Produção de verdade: o que só aparece fora do modo development -O boot saiu do `command:` de uma linha só e virou **`bin/docker-boot`**, com o -motivo de cada passo comentado: +O servidor passou a rodar `RAILS_ENV=production`, e três coisas apareceram: -1. **relógio** — imprime a hora e o fuso no log (se aparecer UTC, o rebuild não pegou); -2. **pid órfão** — sem limpar, o Puma se recusa a subir depois de uma queda; -3. **gems** — `bundle check || bundle install` (o volume `bundle_cache` sombreia os gems da imagem); -4. **migrations** — `db:prepare`, **com espera pelo banco**; -5. **cron** — `whenever --update-crontab` + daemon, *best-effort*; -6. **Puma**. +- **`config.hosts`** — a linha que parecia só liberar o `APP_HOST` na verdade + **liga** a verificação e recusa todo o resto. Agora entram também + `localhost`/`127.0.0.1` (para o `curl` de dentro do NAS servir de diagnóstico) e + a lista `HOSTS_PERMITIDOS`. +- **`i18n.fallbacks = true`** significa "caia no `default_locale`" — que aqui é o + próprio pt-BR. O fallback apontava para si mesmo e chave ausente virava + `Translation missing` **na tela do usuário** (tela de login). Agora `[:en]`, + igual ao `application.rb`, e o `pt-BR.yml` ganhou as traduções do Devise + (`failure`, `sessions`, `passwords`) — o projeto não usa `devise-i18n`. +- **`assume_ssl`** estava fixo, então `request.base_url` virava `https://` e o + `Origin: http://` de quem abre pelo IP não batia: **todo POST voltava 422** + ("recusada por uma verificação de segurança") enquanto os GETs passavam e a tela + parecia normal. Foi o que quebrou a importação do plano. Um gate `FORCE_SSL=false` + devolve o acesso por HTTP para diagnóstico; sem a variável, HTTPS continua + obrigatório. -Ou seja: **a migration é automática a todo restart** — `db:prepare` cria o banco -se não existir, aplica as migrations pendentes e só faz seed em banco novo -(idempotente). Não há passo manual depois do deploy. +> **Proxy reverso:** o destino tem que ser **HTTP** na porta do container. Com +> `https://` o Puma responde +> `Are you trying to open an SSL connection to a non-SSL Puma?`. -> **Por que a espera no passo 4:** o PostgreSQL é externo ao compose. Num reboot -> do servidor o Rails pode subir antes de o banco aceitar conexão — e agora, com -> `restart: unless-stopped`, isso viraria um **loop de reinício** parecendo erro -> de migration. São 10 tentativas × 6s. Se falhar depois disso (migration -> quebrada, credencial errada), o container sai com erro **de propósito**: melhor -> do que servir a aplicação contra um schema desatualizado. +## 6. `db/schema.rb` entrou no `.gitignore` -## Deploy +Nunca foi versionado (o schema vive nas migrations). Como o compose monta +`.:/app`, o `db:prepare` do boot escreve o arquivo **no host** — e um `git add .` +no meio de um rebase o capturou e virou conflito de deploy. Em +`RAILS_ENV=production` ele nem seria gerado (`dump_schema_after_migration = false`). + +> **Deploy é `git fetch` + `git reset --hard origin/`**, não `pull --rebase`: +> o container reescreve `Gemfile.lock` e `db/schema.rb` no host, então o checkout +> produz alteração local sozinho e todo rebase bate de frente com ela. + +## ▶️ Deploy ```bash -# 1. No servidor, uma única vez — relógio com NTP autenticado: -sudo bash deploy/ntp-seguro.sh - -# 2. Rebuild obrigatório (o fuso entra na imagem): -docker compose up -d --build - -# 3. Conferir no log que o container está em -03 e não em UTC: -docker compose logs app | grep '\[boot\] relógio' +sudo bash deploy/ntp-seguro.sh # 1x no servidor +git fetch origin && git reset --hard origin/teste +docker compose up -d --build # --build: o fuso entra na imagem +docker compose logs app | grep '\[boot\]' +curl -I http://127.0.0.1:3001/ # separa "app caiu" de "proxy errado" ``` ## 📂 Arquivos ``` -Dockerfile # TZ=America/Sao_Paulo + tzdata + /etc/localtime; CMD -> bin/docker-boot -whatsapp/Dockerfile # tzdata (Alpine) + mesmo fuso -docker-compose.yml # restart: unless-stopped, TZ, command: bin/docker-boot -bin/docker-boot # boot documentado: migrations com espera, cron, Puma — NOVO -deploy/ntp-seguro.sh # chrony + NTS no host (rodar 1x, como root) — NOVO -.env.example # TZ=America/Sao_Paulo + aviso "fuso ≠ hora" +Dockerfile / whatsapp/Dockerfile # TZ + tzdata + /etc/localtime +docker-compose.yml # restart, PORTA_APP, APP_HOST/NAME, TZ, HOSTS_PERMITIDOS +bin/docker-boot # boot documentado: espera do banco, cron, Puma — NOVO +deploy/ntp-seguro.sh # chrony + NTS no host (1x, root) — NOVO +config/storage.yml # versionado — NOVO +config/environments/production.rb # hosts, i18n fallbacks, gate do FORCE_SSL +config/locales/pt-BR.yml # traduções do Devise +.gitignore # db/schema.rb +.env.example # TZ, PORTA_APP, HOSTS_PERMITIDOS, APP_HOST sem esquema +``` + +
+ +--- + +
+🚚 Romaneio — a tela recuperou as funções do programa antigo + +Referência: o **Romaneiro PDF (GADE Hospitalar)**, o programa em Python que a +operação usava. O PDF já estava fechado; o que faltava era a **tela**. + +## Veículos: lista visível, não um `` com ‹ ›, escolhido para não virar armadilha de scroll com 72 +carros. Resolveu o scroll e criou outro problema: o operador via **um carro por +vez** e perdia a contagem de paradas de cada um — que é justamente a conferência +feita antes de imprimir. + +Agora é a lista inteira, cada item com a contagem (`GADE_038 · 31`), o atual em +laranja, rolagem própria, e as setas ‹ › preservadas para percorrer carro a carro. +Cada veículo é um **link de verdade** (`?veiculo=GADE_038`): volta com o botão +"voltar" do navegador e sobrevive a um F5. No celular vira faixa horizontal — +72 itens empilhados empurrariam a tabela para fora da primeira tela. + +Na mesma coluna, como no original: **motorista do veículo**, **PDF deste veículo** +e **PDF de TODOS os veículos**. + +## Prévia sob demanda, em sobreposição + +Antes o `