Correção de horario e implantação da geração de Romaneio
This commit is contained in:
76
bin/docker-boot
Executable file
76
bin/docker-boot
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Boot do container do Rails (deploy e todo restart do Docker).
|
||||
#
|
||||
# POR QUE ISSO VIROU UM SCRIPT: antes era um `command:` de uma linha só no
|
||||
# docker-compose.yml. Com `restart: unless-stopped` o boot passou a rodar
|
||||
# sozinho — depois de queda, de reboot do servidor e de cada deploy — e um
|
||||
# passo que falha em silêncio vira loop de reinício sem ninguém entender por
|
||||
# quê. Aqui cada etapa diz o que está fazendo no log do container
|
||||
# (`docker compose logs -f app`).
|
||||
#
|
||||
# ORDEM DOS PASSOS (não troque sem ler o motivo):
|
||||
# 1. relógio — só imprime, para diagnóstico (ver comentário abaixo).
|
||||
# 2. pid órfão — se o container morreu sem limpar, o Puma se recusa a subir.
|
||||
# 3. gems — o volume bundle_cache SOMBREIA os gems da imagem, então
|
||||
# gem nova no Gemfile não chega no container só com --build.
|
||||
# 4. migrations — db:prepare: cria o banco se não existir, roda as migrations
|
||||
# pendentes e só faz seed em banco recém-criado (idempotente).
|
||||
# 5. cron — grava o crontab do config/schedule.rb e sobe o daemon.
|
||||
# 6. Puma — processo em primeiro plano (é ele que segura o container).
|
||||
set -uo pipefail
|
||||
|
||||
log() { echo "[boot] $*"; }
|
||||
|
||||
# ── 1. Relógio ───────────────────────────────────────────────────────────────
|
||||
# O container NÃO tem relógio próprio: ele lê o clock do kernel do HOST. Não
|
||||
# adianta rodar NTP aqui dentro (exigiria CAP_SYS_TIME e mexeria na hora do
|
||||
# servidor inteiro) — quem sincroniza é o host, via chrony/NTS; ver
|
||||
# deploy/ntp-seguro.sh. O que é do container é só o FUSO (TZ), fixado no
|
||||
# Dockerfile. Se a linha abaixo aparecer em UTC, o rebuild da imagem não pegou.
|
||||
log "relógio do container: $(date '+%Y-%m-%d %H:%M:%S %Z (%z)') | UTC: $(date -u '+%H:%M:%S')"
|
||||
|
||||
# ── 2. PID órfão do Puma ─────────────────────────────────────────────────────
|
||||
rm -f tmp/pids/server.pid
|
||||
|
||||
# ── 3. Gems ──────────────────────────────────────────────────────────────────
|
||||
if ! bundle check >/dev/null 2>&1; then
|
||||
log "gem faltando no volume bundle_cache — rodando bundle install"
|
||||
bundle install || { log "ERRO: bundle install falhou"; exit 1; }
|
||||
fi
|
||||
|
||||
# ── 4. Migrations ────────────────────────────────────────────────────────────
|
||||
# O PostgreSQL é EXTERNO ao compose: num reboot do servidor o Rails pode subir
|
||||
# antes do banco aceitar conexão. Sem a espera abaixo o container sairia com
|
||||
# erro e o `restart: unless-stopped` ficaria reiniciando em loop — parecendo
|
||||
# problema de migration quando é só ordem de boot. 10 tentativas × 6s = 1 min.
|
||||
tentativas=10
|
||||
for i in $(seq 1 $tentativas); do
|
||||
log "migrations (db:prepare) — tentativa $i/$tentativas"
|
||||
if bundle exec rails db:prepare; then
|
||||
log "banco pronto e migrations aplicadas"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq "$tentativas" ]; then
|
||||
# Aqui é falha DE VERDADE (migration quebrada ou credencial errada): sair
|
||||
# com erro é melhor do que subir a aplicação contra um schema desatualizado.
|
||||
log "ERRO: db:prepare falhou após $tentativas tentativas — veja o erro acima"
|
||||
exit 1
|
||||
fi
|
||||
log "banco indisponível ou migration falhou; nova tentativa em 6s"
|
||||
sleep 6
|
||||
done
|
||||
|
||||
# ── 5. Cron (best-effort) ────────────────────────────────────────────────────
|
||||
# Best-effort de propósito: agendamento quebrado atrasa um relatório, mas NUNCA
|
||||
# pode impedir o sistema de subir. As variáveis do banco chegam ao job pelo
|
||||
# dotenv quando o rake carrega o Rails — o cron não precisa herdar o ENV.
|
||||
if bundle exec whenever --update-crontab && cron; then
|
||||
log "cron no ar (jobs do config/schedule.rb, no fuso ${TZ:-?})"
|
||||
else
|
||||
log "AVISO: cron/crontab indisponível (rode com --build) — seguindo sem agendamento"
|
||||
fi
|
||||
|
||||
# ── 6. Puma ──────────────────────────────────────────────────────────────────
|
||||
log "subindo o Rails"
|
||||
exec bundle exec rails s -b 0.0.0.0
|
||||
149
bin/sondar_plano_do_dia
Executable file
149
bin/sondar_plano_do_dia
Executable file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
#
|
||||
# Descobre COMO a API do SimpliRoute expõe o par veículo/ordem de um plano — que é
|
||||
# o que o romaneio ("CONTROLE DE ENTREGA") precisa para saber quantas folhas
|
||||
# imprimir e em que sequência.
|
||||
#
|
||||
# ── POR QUE ESTE SCRIPT EXISTE ──────────────────────────────────────────────
|
||||
# Romaneios::PlanoDoDia tem uma escada de queda:
|
||||
# (a) /v1/routes/routes/ traz vehicle + lista ordenada de visitas
|
||||
# (b) a própria visita traz alguma chave de veículo/ordem
|
||||
# (c) nada disso -> espelho local (db_reem_simplerout_2026), por vehicle + eta
|
||||
# (d) espelho vazio -> manda o operador subir o .xlsx do plano
|
||||
# As rungs (a) e (b) NUNCA foram verificadas: não há SIMPLIROUTE_TOKEN na máquina
|
||||
# de desenvolvimento. Este script fecha isso.
|
||||
#
|
||||
# ── RESULTADO DA RODADA DE __/__/____ ───────────────────────────────────────
|
||||
# (preencher depois de rodar, como está em bin/sondar_busca_nf)
|
||||
# [ ] /v1/routes/routes/ existe? status: ___ rotas: ___
|
||||
# [ ] a rota traz vehicle? ___
|
||||
# [ ] a rota embute a lista de visitas? ___
|
||||
# [ ] a visita traz vehicle/order? ___
|
||||
# [ ] veículos pela API x pelo espelho: ___ x ___
|
||||
# => rung usada: ___
|
||||
#
|
||||
# SÓ LEITURA: o script faz apenas GET. Não altera nada.
|
||||
#
|
||||
# Rode NO SERVIDOR (é lá que vive o SIMPLIROUTE_TOKEN):
|
||||
#
|
||||
# SIMPLIROUTE_TOKEN=xxx bin/sondar_plano_do_dia --data 2026-08-31
|
||||
#
|
||||
# --data precisa ser um dia com plano PUBLICADO (senão a resposta vem vazia e não
|
||||
# prova nada).
|
||||
#
|
||||
# ⚠️ Por que olhar os VALORES e não só as chaves: a API é Django REST e devolve
|
||||
# chave existente com valor nil o tempo todo. "A visita tem a chave `vehicle`" NÃO
|
||||
# significa que ela venha preenchida — a prova é o valor.
|
||||
|
||||
require 'net/http'
|
||||
require 'json'
|
||||
require 'uri'
|
||||
require 'date'
|
||||
|
||||
BASE = ENV.fetch('SIMPLIROUTE_BASE_URL', 'https://api.simpliroute.com')
|
||||
TOKEN = ENV['SIMPLIROUTE_TOKEN'].to_s
|
||||
|
||||
CHAVES_INTERESSE = /vehicle|route|driver|order|sequence|position/i.freeze
|
||||
|
||||
def sair(msg)
|
||||
warn msg
|
||||
exit 1
|
||||
end
|
||||
|
||||
sair('Defina SIMPLIROUTE_TOKEN no ambiente.') if TOKEN.empty?
|
||||
|
||||
data = nil
|
||||
ARGV.each_with_index { |a, i| data = ARGV[i + 1] if a == '--data' }
|
||||
sair('Uso: bin/sondar_plano_do_dia --data <YYYY-MM-DD>') if data.to_s.empty?
|
||||
Date.parse(data) rescue sair("Data inválida: #{data}")
|
||||
|
||||
def get(caminho)
|
||||
uri = URI.join(BASE, caminho)
|
||||
req = Net::HTTP::Get.new(uri)
|
||||
req['Authorization'] = "Token #{TOKEN}"
|
||||
req['Accept'] = 'application/json'
|
||||
|
||||
inicio = Time.now
|
||||
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
|
||||
open_timeout: 10, read_timeout: 120) do |http|
|
||||
http.request(req)
|
||||
end
|
||||
[res.code.to_i, res.body.to_s, Time.now - inicio]
|
||||
rescue StandardError => e
|
||||
[0, e.message, Time.now - inicio]
|
||||
end
|
||||
|
||||
def json(corpo)
|
||||
JSON.parse(corpo)
|
||||
rescue JSON::ParserError
|
||||
nil
|
||||
end
|
||||
|
||||
def amostra(hash)
|
||||
hash.select { |k, _| k.to_s.match?(CHAVES_INTERESSE) }
|
||||
.map { |k, v| " #{k} = #{v.inspect.slice(0, 90)}" }
|
||||
end
|
||||
|
||||
puts "== SONDAGEM DO PLANO DE #{data} =="
|
||||
puts
|
||||
|
||||
# ── 1. Visitas do dia ───────────────────────────────────────────────────────
|
||||
codigo, corpo, seg = get("/v1/routes/visits/?planned_date=#{data}")
|
||||
puts "[1] GET /v1/routes/visits/?planned_date=#{data}"
|
||||
puts " HTTP #{codigo} · #{corpo.bytesize} bytes · #{seg.round(2)}s"
|
||||
|
||||
visitas = json(corpo)
|
||||
sair(' Resposta não é JSON — abortando.') unless visitas.is_a?(Array)
|
||||
puts " visitas: #{visitas.size}"
|
||||
sair(' Nenhuma visita nessa data. Escolha um dia com plano publicado.') if visitas.empty?
|
||||
|
||||
primeira = visitas.first
|
||||
puts " chaves: #{primeira.keys.sort.join(', ')}"
|
||||
puts ' chaves de interesse (com VALOR — chave nil não serve):'
|
||||
linhas = amostra(primeira)
|
||||
puts(linhas.empty? ? ' (nenhuma)' : linhas)
|
||||
|
||||
preenchidas = primeira.select { |k, v| k.to_s.match?(CHAVES_INTERESSE) && !v.nil? && v.to_s.strip != '' }
|
||||
puts " => rung (b) viável? #{preenchidas.keys.any? { |k| k.to_s.match?(/vehicle/i) } ? 'SIM' : 'NÃO'}"
|
||||
puts
|
||||
|
||||
# ── 2. Rotas do dia ─────────────────────────────────────────────────────────
|
||||
codigo, corpo, seg = get("/v1/routes/routes/?planned_date=#{data}")
|
||||
puts "[2] GET /v1/routes/routes/?planned_date=#{data}"
|
||||
puts " HTTP #{codigo} · #{corpo.bytesize} bytes · #{seg.round(2)}s"
|
||||
|
||||
rotas = json(corpo)
|
||||
if rotas.is_a?(Array) && rotas.any?
|
||||
rota = rotas.first
|
||||
puts " rotas: #{rotas.size}"
|
||||
puts " chaves: #{rota.keys.sort.join(', ')}"
|
||||
puts ' chaves de interesse:'
|
||||
puts(amostra(rota).then { |l| l.empty? ? ' (nenhuma)' : l })
|
||||
visits = rota['visits']
|
||||
puts " embute 'visits'? #{visits.nil? ? 'NÃO' : "SIM (#{Array(visits).size} itens, #{Array(visits).first.class})"}"
|
||||
tem_veiculo = %w[vehicle vehicle_name vehicle_id].any? { |k| rota[k].to_s.strip != '' }
|
||||
puts " => rung (a) viável? #{tem_veiculo && !visits.nil? ? 'SIM' : 'NÃO'}"
|
||||
else
|
||||
puts " sem rotas utilizáveis (corpo: #{corpo.slice(0, 200)})"
|
||||
puts ' => rung (a) NÃO viável'
|
||||
end
|
||||
puts
|
||||
|
||||
# ── 3. Cruzamento com o espelho ─────────────────────────────────────────────
|
||||
# Quantos veículos a API enxerga? O número tem que bater com o do espelho local;
|
||||
# se não bater, alguma rota está ficando de fora e o romaneio sairia incompleto.
|
||||
veiculos_api = visitas.filter_map { |v| %w[vehicle vehicle_name].filter_map { |k| v[k] }.first }
|
||||
.map(&:to_s).reject(&:empty?).uniq
|
||||
if rotas.is_a?(Array)
|
||||
veiculos_api |= rotas.filter_map { |r| %w[vehicle vehicle_name].filter_map { |k| r[k] }.first }
|
||||
.map(&:to_s).reject(&:empty?)
|
||||
end
|
||||
puts "[3] veículos distintos vistos pela API: #{veiculos_api.size}"
|
||||
puts " #{veiculos_api.sort.first(10).join(', ')}#{'…' if veiculos_api.size > 10}"
|
||||
puts
|
||||
puts ' Compare com o espelho, no console do app:'
|
||||
puts " Entrega.da_conta_gade.no_periodo(Date.parse('#{data}'), Date.parse('#{data}'))" \
|
||||
'.distinct.count(:vehicle)'
|
||||
puts
|
||||
puts 'Anote o resultado no cabeçalho deste arquivo antes de commitar.'
|
||||
Reference in New Issue
Block a user