Sistema web simples, focado em mobile, para registrar abastecimentos e manutenções de veículos e acompanhar consumo (km/l) e gastos. Acesse em https://pitstop.morenadoaco.com.br.
- Multi-usuário: cada conta só enxerga e mexe nos próprios veículos/registros
- Cadastro aberto com confirmação de e-mail (código de 6 dígitos, válido por 15 min, com limite de tentativas e reenvio), rate limit de 5 cadastros por hora por IP; também dá pra entrar por convite de quem já usa o app (link com token de uso único e validade de 7 dias, e-mail considerado confirmado automaticamente nesse caso)
- Painel Administrativo (conta com papel "admin"): visão agregada de todas as contas — veículos, registros e total gasto por conta — sem acessar o detalhe de nenhum registro individual
- Funciona sem internet: Service Worker + fila offline (IndexedDB) guardam o que você registra sem sinal e sincronizam sozinhos assim que a conexão volta (inclusive em segundo plano, via Background Sync); aviso de "o que mudou" nas primeiras aberturas depois de uma atualização
- Login com bloqueio temporário após tentativas falhas, opção de lembrar o e-mail no aparelho (sem guardar senha) e botão de mostrar/ocultar senha nos campos de senha
- Recuperação de senha esquecida por link de e-mail (token único de uso único, válido por 1h, sem revelar se o e-mail existe na base)
- Cadastro, edição e exclusão de veículos (nome, tipo, cor, placa) com busca inteligente de modelo (ex.: "Bros 160 2025") que autopreenche capacidade do tanque e peso a partir de um catálogo de modelos comuns no Brasil — campos continuam editáveis à mão se o modelo não estiver no catálogo
- Registro, edição e exclusão de abastecimentos (km, litros, valor pago, combustível: Gasolina Comum/Aditivada, Etanol, Diesel, GNV ou Outro), manutenções (km, valor, descrição) e despesas (Seguro, IPVA, Estacionamento, Pedágio, Multa, Lavagem ou Outro)
- Lembretes de manutenção/documentos por km ou por data (ex.: troca de óleo aos 40.000km, seguro vencendo em uma data), com status Vencido/Próximo/Em dia, alerta no painel principal e notificação push (Minha Conta → Notificações) avisando mesmo com o app fechado
- Conquistas (gamificação): sequência de meses seguidos registrando, e selos por marco — Primeira Carga, 10 Abastecimentos, Motorista Veterano (50), Economia do Mês (consumo médio melhor que o mês anterior) e Manutenção em Dia — calculados na hora a partir dos próprios dados, exibidos no painel principal
- Cálculo automático da última média de consumo (km/l), do preço por litro, do gasto do mês e da autonomia estimada do tanque cheio (capacidade do tanque × última média)
- Meta de gasto mensal configurável (Minha Conta), com barra de progresso colorida no painel principal comparando o gasto do mês com a meta (verde/amarelo/vermelho) e projeção de gasto até o fim do mês no ritmo atual
- Calculadora Etanol × Gasolina (regra dos 70%, com limiar ajustável) pra saber na hora qual combustível compensa mais
- Relatórios com gráficos (Chart.js): gasto por mês, km rodado por mês, evolução do consumo e distribuição do gasto por categoria (combustível, manutenção, cada tipo de despesa), cards de gasto total, custo por km, gasto médio por dia e preço médio por litro, atalhos de período (este mês, últimos 30 dias, este ano) além do filtro manual por veículo e por data, exportação em CSV ou PDF (impressão do navegador), comparação do consumo real com o de fábrica (cidade/estrada) quando o veículo tem modelo do catálogo vinculado, e tabela comparando consumo/custo por km/gasto entre todos os veículos de quem tem 2 ou mais cadastrados
- Filtro de registros e relatórios por veículo
- Conformidade com a LGPD: política de privacidade, aceite de consentimento obrigatório no cadastro/convite e exclusão definitiva da própria conta e dados (direito ao esquecimento)
- Identidade visual própria (paleta laranja + teal, logo, favicons), com medidor (gauge) de consumo, selos de ícone coloridos nas estatísticas, transições e micro-interações em toda a interface, e manifest PWA (instalável na tela inicial)
- App Android nativo (TWA assinado) pra instalar via APK, além do PWA — página
/instalar.phpcom instruções pras duas formas - Interface mobile-only por design (Bootstrap 5 + Bootstrap Icons) com navegação inferior fixa e botão de novo registro embutido na própria barra (elevado, ao centro) — mesmo layout em qualquer tamanho de tela, só centralizado numa largura de celular a partir de 560px (sem layout separado de desktop/notebook)
- Estados vazios com ícone, texto e chamada para ação em vez de mensagens soltas
- Frontend: HTML5 + Bootstrap 5 (CDN) + Bootstrap Icons + Chart.js (CDN) + identidade visual própria (CSS, com animações) + manifest PWA + Service Worker/IndexedDB (modo offline)
- Backend: PHP 8.2 puro (sem framework), Apache
- Banco: MySQL 8.0, acesso exclusivo via PDO (prepared statements)
- E-mail: cliente SMTP próprio em PHP puro (sem dependências), usado pro envio de convites
- Notificações push: minishlink/web-push (única dependência via Composer do projeto — VAPID + criptografia do payload são delicados demais pra reinventar na mão) rodando num serviço
crondedicado - App Android: TWA (Trusted Web Activity) gerado com Bubblewrap, assinado com keystore próprio
- Infra: Docker Compose (build próprio da imagem PHP+Apache hardenizada)
- Testes: PHPUnit (dependência de desenvolvimento,
composer installsem--no-dev) — testes unitários da lógica de negócio pura e testes de integração contra MySQL real; gate automático em CI (ver Testes)
pitstop-br/
├── docker-compose.yml
├── phpunit.xml # config do PHPUnit (bootstrap, suítes Unit/Integration)
├── .env # credenciais (gitignored, gerado localmente)
├── db/
│ └── init.sql # schema + seed inicial
├── docker/php/
│ ├── Dockerfile # imagem PHP+Apache hardenizada
│ ├── php.ini # hardening PHP (expose_php off, sessão segura, sem upload...)
│ └── security.conf # hardening Apache (headers, sem listagem de diretório)
├── tests/
│ ├── bootstrap.php # autoload + require das funções testadas
│ ├── Unit/ # lógica de negócio pura (sem banco) + PDO SQLite em memória
│ └── Integration/ # fluxos que exigem MySQL real (window functions, FOR UPDATE...)
└── src/
├── api/
│ ├── registro.php # POST idempotente (client_uuid) usado pela fila offline
│ ├── lembrete.php # POST idempotente (client_uuid) usado pela fila offline
│ └── versao.php # versão + changelog em JSON (aviso de atualização)
├── assets/
│ ├── css/brand.css # identidade visual (paleta, header, bottom-nav, telas de auth)
│ ├── img/ # logo, favicons e ícones PWA
│ └── js/
│ ├── offline.js # registra o SW, intercepta formulários sem sinal, aviso de atualização
│ └── idb-outbox.js # fila offline (IndexedDB), compartilhada com o Service Worker
├── config/
│ ├── bootstrap.php # sessão segura (30 dias, PWA) + carrega conexão/CSRF/auth/funções/versão
│ ├── conexao.php # PDO (lê credenciais do ambiente)
│ ├── csrf.php # geração/validação de token CSRF
│ ├── auth.php # login/registro/logout/guard, papel admin, verificação de e-mail, lockout
│ ├── versao.php # versão do app + changelog (rodapé e aviso de atualização)
│ └── mailer.php # cliente SMTP mínimo (sem dependências) pro envio de convites/códigos
├── includes/
│ ├── functions.php # helpers (escape, flash, cálculo de consumo, validação de registro/lembrete)
│ ├── header.php
│ └── footer.php
├── manifest.json # manifest PWA (instalável na tela inicial)
├── sw.php # Service Worker (cache com versionamento, fallback offline, Background Sync)
├── login.php / cadastro.php / verificar_email.php / logout.php # autenticação + confirmação de e-mail
├── esqueci_senha.php / redefinir_senha.php # recuperação de senha por link de e-mail (token único, 1h)
├── convidar.php / convite.php # envio e aceite de convite (registro por convite)
├── gerenciador.php # painel administrativo (dados agregados por conta; só para papel admin)
├── conta.php / privacidade.php # minha conta (exclusão de dados) e política LGPD
├── combustivel.php # calculadora Etanol x Gasolina (regra dos 70%)
├── index.php # dashboard (última média, gastos do mês + projeção, alerta de lembretes, registros)
├── relatorios.php # gráficos de gasto/km/consumo/categorias, custo por km, comparação entre
│ # veículos, atalhos de período; filtro por período; export CSV/PDF
├── adicionar.php / registro_editar.php / excluir.php # CRUD de registros (abastecimento/manutenção/despesa)
├── lembretes.php / lembrete_concluir.php / lembrete_excluir.php # lembretes de manutenção (km ou data)
└── veiculos.php / veiculo_editar.php / veiculo_excluir.php # CRUD de veículos
- 100% PDO com prepared statements (sem SQL cru/concatenado) — zero SQLi
- Proteção CSRF (token por sessão,
hash_equals) em todo formulário POST - Output sempre escapado (
htmlspecialchars) — zero XSS refletido - Validação estrita (whitelist) de tipo de registro, tipo de veículo, datas e números
- Autenticação: senha com
password_hash/password_verify, bloqueio de 15 min após 5 tentativas falhas, mensagem de erro genérica no login (sem enumeração de e-mail),session_regenerate_idapós login/cadastro - Isolamento multi-usuário: toda consulta/gravação de veículo e registro é restrita por
usuario_id(via FK +JOIN/WHERE), prevenindo IDOR entre contas - Confirmação de e-mail: código de 6 dígitos, armazenado só como hash SHA-256 (nunca em texto plano), validade de 15 min, limite de tentativas e rate limit de reenvio; sem essa confirmação a conta não consegue logar
- Rate limit de cadastro (5 por hora por IP, hash do IP no banco) contra automação de contas em massa
- Papel admin: página administrativa responde 404 (não 403) pra quem não é admin, evitando revelar que a rota existe; painel mostra só dados agregados por conta, nunca o detalhe de um registro
- API offline (
api/registro.php,api/lembrete.php) reusa a mesma validação e o mesmo escopo porusuario_iddos formulários clássicos; inserções são idempotentes porclient_uuid(UNIQUE), então reenviar o mesmo item da fila offline nunca duplica dados - Convites: token de 32 bytes aleatórios, armazenado só como hash SHA-256 no banco (nunca em
texto plano), expira em 7 dias, uso único garantido por lock transacional (
SELECT ... FOR UPDATE) - Exclusão de conta exige reautenticação por senha antes de apagar os dados definitivamente
- Sessão: cookie
HttpOnly,SameSite=Strict,Secure(atrás de proxy HTTPS) - Apache:
ServerTokens Prod, sem listagem de diretório, headersCSP/X-Frame-Options/X-Content-Type-Options/Referrer-Policy, bloqueio de arquivos sensíveis (.env,.sql, etc.) - PHP:
expose_php=Off,display_errors=Off, uploads desabilitados, limites de memória/execução - Container do app:
read_onlyfilesystem,cap_drop: ALL(com apenas as 3 capabilities mínimas necessárias),no-new-privileges, sem privilégio de root persistente - MySQL sem porta exposta ao host — acessível apenas pela rede Docker interna
- Senhas geradas aleatoriamente (
openssl rand), armazenadas só em.env(gitignored,chmod 600) - Limites de CPU/memória por container (
deploy.resources.limits)
Pra enviar convites por e-mail, defina no .env (raiz do projeto, gitignored):
SMTP_HOST=smtp.exemplo.com
SMTP_PORT=465
SMTP_SECURE=true
SMTP_USER=noreply@pitstop.morenadoaco.com.br
SMTP_PASS=...
SMTP_FROM=PitStop BR <noreply@pitstop.morenadoaco.com.br>
Sem essas variáveis, o convite continua sendo gerado no banco normalmente, mas o e-mail não é
enviado (fica registrado em log). O cliente SMTP é caseiro (sem dependências externas), suporta
TLS implícito (porta 465) ou STARTTLS (porta 587) com AUTH LOGIN.
Pra ativar a notificação push dos lembretes, gere um par de chaves VAPID (uma vez só, por
ambiente) e defina no .env:
docker run --rm -v "$(pwd)/src:/app" -w /app composer:2 exec vendor/bin/web-push-vapid-genVAPID_PUBLIC_KEY=...
VAPID_PRIVATE_KEY=...
VAPID_SUBJECT=mailto:contato@seudominio.com.br
Sem essas variáveis, o botão "Ativar notificações" some de Minha Conta e o serviço cron só fica
registrando os lembretes vencidos sem enviar nada (nenhum erro, só não notifica).
Convite e redefinição de senha mandam um link absoluto por e-mail. Defina no .env:
APP_URL=https://pitstop.morenadoaco.com.br
Sem essa variável, o link é montado a partir do Host da requisição (fallback só pra
desenvolvimento local) — em produção, APP_URL fixa evita que um Host forjado no request
troque o domínio do link mandado pro usuário (host header poisoning).
cd pitstop-br
scripts/composer-install.sh # instala src/vendor/ (dependências PHP) via Docker — só na 1ª vez ou quando composer.json mudar
docker compose up -d --buildApp disponível em http://127.0.0.1:8033 (atrás de proxy reverso Nginx + TLS em produção).
O projeto usa PHPUnit (dependência de desenvolvimento, não vai pra produção —
scripts/composer-install.sh roda composer install --no-dev). Duas suítes:
- Unit (
tests/Unit/): lógica de negócio pura desrc/includes/functions.phpesrc/config/{auth,csrf}.php(validação/whitelist de registro e lembrete, cálculo de status de lembrete, escape de HTML, sanitização de célula CSV, cadastro/login/lockout). Não precisa de MySQL — as poucas funções que consultam o banco usam um PDO SQLite em memória (a query em si é portável entre os dois; vertests/Unit/Fixtures/SqliteFixture.php). - Integration (
tests/Integration/): fluxos que dependem de recursos exclusivos do MySQL — window functions (LAG() OVER),DATE_FORMAT,GREATEST,SELECT ... FOR UPDATE— usados no cálculo de km/l (calcularUltimaMedia), estatísticas de veículo, detecção de anomalias (detectarAnomaliasRegistro), conquistas e nos fluxos de verificação de e-mail/redefinição de senha. Precisa de um banco MySQL de teste (nunca o de produção); sem a envDB_HOSTdefinida, a suíte inteira é pulada (markTestSkipped), não falha.
Rodar só os testes unitários (sem precisar de Docker/MySQL, só PHP 8.2 + Composer):
docker run --rm -v "$(pwd)/src:/app" -w /app composer:2 install # instala com dev deps
docker run --rm -v "$(pwd):/app" -w /app php:8.2-cli php src/vendor/bin/phpunit --testsuite UnitRodar a suíte completa (unit + integration) contra um MySQL de teste descartável:
docker network create pitstop-test-net
docker run -d --name pitstop-test-mysql --network pitstop-test-net \
-e MYSQL_ROOT_PASSWORD=root_test_pw -e MYSQL_DATABASE=pitstop_test \
-e MYSQL_USER=pitstop_test -e MYSQL_PASSWORD=pitstop_test_pw mysql:8.0
# aguarde o healthcheck e carregue o schema:
docker exec -i pitstop-test-mysql mysql -uroot -proot_test_pw pitstop_test < db/init.sql
docker run --rm --network pitstop-test-net -v "$(pwd):/app" -w /app \
-e DB_HOST=pitstop-test-mysql -e DB_NAME=pitstop_test \
-e DB_USER=pitstop_test -e DB_PASS=pitstop_test_pw \
php:8.2-cli bash -c "docker-php-ext-install -j$(nproc) pdo_mysql >/dev/null && php src/vendor/bin/phpunit"
docker rm -f pitstop-test-mysql && docker network rm pitstop-test-netEm CI (.github/workflows/docker-publish.yml), o job test faz exatamente isso (MySQL como
services: do GitHub Actions) em todo push e pull request, como gate antes do job build-and-push
— a imagem só builda/publica no GHCR se a suíte inteira passar.
Limitação conhecida: como o roteamento é manual por arquivo (sem framework/front controller),
as próprias páginas (adicionar.php, veiculos.php, relatorios.php etc.) misturam sessão,
CSRF, PDO e HTML/redirect na mesma requisição — não são testáveis por PHPUnit sem um refactor
grande de arquitetura (extrair a lógica de cada página pra uma função pura, como já é o caso de
validarRegistro/validarLembrete/calcularEstatisticasVeiculo). Os testes desta suíte cobrem
toda a lógica de negócio e validação que já vive em funções (includes/functions.php,
config/auth.php, config/csrf.php); o comportamento HTTP das páginas em si (rotas, redirects,
render) continua dependendo de teste manual.
A versão exibida no app (rodapé, aviso de atualização) e o changelog em linguagem simples vivem em
src/config/versao.php (APP_VERSION/APP_CHANGELOG). Ao dar bump nessa versão e mergear em
main, o workflow .github/workflows/release.yml cria sozinho a tag e a Release
correspondente no GitHub (vX.Y.Z), usando o próprio texto de APP_CHANGELOG como notas — sem
passo manual e sem duplicar o changelog em outro lugar. Isso, por sua vez, dispara o
docker-publish.yml já existente, que builda e publica a imagem no GHCR com a tag semver da
release. Basta então atualizar a tabela abaixo (histórico técnico, mais detalhado que o do app) no
mesmo commit.
| Versão | Data | Descrição |
|---|---|---|
| 1.16.1 | 2026-07-11 | Comparador de custo em combustivel.php: novo card lado a lado mostrando quanto custa encher N litros (input +/- e stepper, padrão = capacidade do tanque quando o usuário tem só 1 veículo cadastrado com esse dado, senão 12L) em Etanol vs Gasolina, com o mais barato destacado (borda + selo). Complementa (não substitui) o veredito existente de "compensa trocar" — aquele considera rendimento por km, este é custo absoluto direto |
| 1.16.0 | 2026-07-11 | Relatórios renovados (relatorios.php): (1) agrupamento configurável dia/semana/mês nos gráficos "Gasto por Período" e "Km Rodado por Período" (chave de agrupamento sempre uma DATE real — DATE(r.data)/DATE_SUB(r.data, INTERVAL WEEKDAY(r.data) DAY)/DATE_FORMAT(r.data, "%Y-%m-01") — pra ordenar certo nos três casos; rótulo bonito formatado em PHP via formatarRotuloPeriodo()); (2) atalhos de período novos (Últimos 7 dias, Mês passado, Semana passada); (3) card novo "Km Rodado"; (4) "Total Gasto" ganha badge de variação percentual vs. o período anterior de mesmo tamanho; (5) gráficos com paleta categórica validada pela skill dataviz (a antiga falhava banda de luminosidade/chroma/contraste — nova paleta com todos os 6 checks OK, cor fixa por nome de categoria em vez de por posição no ranking) e tooltips/eixos com formatação de moeda/km/km-por-litro em vez do padrão cru do Chart.js |
| 1.15.3 | 2026-07-11 | Nova calcularUltimaMediaEstimativa() (functions.php) — mesma conta simples de antes da v1.15.0 (km desde o abastecimento anterior ÷ litros do atual), SEM exigir tanque cheio, usada só pra alimentar um número provisório no dashboard quando calcularUltimaMedia() não tem trecho fechado. Isolada de propósito das estatísticas oficiais (calcularEstatisticasVeiculo) e da detecção de consumo em queda (detectarAnomaliasRegistro) — lá, continua exigindo tanque cheio confirmado, senão geraria alerta falso ou distorceria a comparação entre veículos. Na tela, vem sempre com aviso visível de "estimativa não confirmada". 4 novos testes (102 no total), incluindo o cenário real que motivou a mudança (nenhum dos dois abastecimentos do usuário era tanque cheio) |
| 1.15.2 | 2026-07-11 | UX: "Última Média" no dashboard distingue "sem histórico nenhum" de "aguardando o próximo tanque cheio" — antes os dois casos mostravam o mesmo "Sem dados", genérico o bastante pra parecer bug pro usuário logo depois da correção da v1.15.0. Nova variável $aguardandoTanqueCheio em index.php (2+ abastecimentos registrados mas calcularUltimaMedia() retornou null) troca a mensagem por "Aguardando" com a explicação |
| 1.15.1 | 2026-07-11 | Correção do bug da v1.15.0, parte 2: relatorios.php tinha sua própria query LAG-based pra "Evolução do Consumo" (gráfico), independente das 3 funções corrigidas em functions.php — não respeitava tanque_cheio, então continuava mostrando km/l impossível. calcularTrechosConsumo() ganhou o campo data no retorno (rótulo do gráfico) e relatorios.php passou a chamá-la (por veículo, juntando os trechos quando não há filtro de veículo — igual ao comportamento anterior de misturar veículos na mesma linha). Também corrigido, no mesmo commit, o workflow release.yml: a Release automática da v1.15.0 falhou porque as notas (texto livre do changelog) eram interpoladas direto no run: do bash via ${{ }} — aspas dentro do texto ("Encheu o tanque") fechavam a citação do shell mais cedo e quebravam o comando (no matches found). Agora passa por variável de ambiente ("$RELEASE_NOTES"), imune a qualquer caractere especial no texto |
| 1.15.0 | 2026-07-11 | Correção de bug real: km/l podia dar um valor absurdo (ex.: 69 km/l numa moto de ~30 km/l real) quando um abastecimento era só um complemento parcial, não um tanque cheio — o cálculo ("cheio a cheio": km desde o abastecimento anterior ÷ litros do abastecimento atual) assumia sempre tanque cheio, sem meio de o usuário dizer o contrário. Nova coluna registros.tanque_cheio (migração db/migrations/0002_tanque_cheio.sql, DEFAULT 1 preserva o histórico existente) + checkbox "Encheu o tanque" em Adicionar/Editar Registro. Nova função calcularTrechosConsumo() em functions.php centraliza a lógica (antes duplicada 3x, cada cópia com o mesmo bug): acumula os litros de complementos parciais até o próximo tanque cheio confirmado, só então fecha o trecho — usada por calcularUltimaMedia(), calcularEstatisticasVeiculo() e a detecção de "consumo em queda" de detectarAnomaliasRegistro(). Fila offline (offline.js) ajustada pra sempre mandar o campo explícito (checkbox desmarcado não entra em FormData, senão o servidor entenderia como tanque cheio por omissão). Suíte de testes (98 testes) continua passando sem alteração — o DEFAULT 1 faz o comportamento de todo registro pré-existente permanecer idêntico |
| 1.14.1 | 2026-07-09 | Testes automatizados (o projeto não tinha nenhum): PHPUnit configurado via Composer (require-dev, fora do build de produção). Suíte Unit (70 testes, sem banco) cobre a lógica de negócio pura de functions.php/auth.php/csrf.php — validação/whitelist de registro e lembrete, cálculo de status de lembrete, escape/sanitização (CSV/Formula Injection), cadastro e login (lockout progressivo, conta bloqueada, e-mail não verificado), usando um PDO SQLite em memória pras poucas consultas cuja SQL é portável. Suíte Integration (28 testes) cobre, contra um MySQL real, o que depende de recursos exclusivos dele (window functions, DATE_FORMAT, GREATEST, SELECT ... FOR UPDATE): cálculo de km/l (calcularUltimaMedia) e estatísticas de veículo, as 3 detecções de detectarAnomaliasRegistro, idempotência de inserirRegistro/inserirLembrete via client_uuid, conquistas e os fluxos completos de verificação de e-mail/redefinição de senha; pulada (não falha) sem um banco de teste disponível. Novo job test em docker-publish.yml roda a suíte inteira (MySQL como serviço do GitHub Actions) em todo push/PR, como gate antes do build/push da imagem pro GHCR — build só acontece se os testes passarem. Nenhum arquivo servido em produção foi alterado (dependência de teste e tests/ ficam fora do --no-dev do deploy); ver seção Testes para como rodar localmente e a limitação conhecida (páginas em si, sem extrair a lógica pra função, continuam fora do escopo do PHPUnit sem um refactor de arquitetura maior) |
| 1.14.0 | 2026-07-06 | Alertas inteligentes: nova tabela alertas detecta, a partir do histórico de registros, três anomalias por abastecimento — consumo 20%+ pior que a média do veículo, preço/L 15%+ acima da média e odômetro menor/igual ao último já registrado (esse último vale pra qualquer tipo de registro, não só abastecimento). Detecção em detectarAnomaliasRegistro() (functions.php), chamada logo após inserirRegistro() em adicionar.php e api/registro.php — inserirRegistro() passou a retornar ['id','novo'] pra fila offline não reprocessar (e duplicar alerta) num replay idempotente do mesmo client_uuid. Painel principal ganha bloco de alertas não lidos (dispensável, api/alerta_lido.php) e novo cron enviar_alertas_push.php (mesmo padrão de dedupe de lembretes, via push_notificado_em). Segurança: cadastro com e-mail já existente agora responde de forma indistinguível de um cadastro novo (mesmo HTTP 200, mesma tela, mesmo password_hash() "inútil" pra igualar o tempo) — e-mail avisa o dono de verdade, sem código nem login automático; APP_URL fixa via env substitui Host da requisição nos links de convite/redefinição de senha (fecha host header poisoning); sessão ganha timeout de inatividade de 7 dias além do cookie de 30 (bootstrap.php); CSP ganha report-uri (endpoint novo api/csp_report.php, sem auth/CSRF — é o navegador quem chama) e HSTS ganha preload. Also reconciliado o drift entre db/init.sql e o schema real em produção (usuarios.role/email_verificado_em, registros/lembretes.client_uuid, tabelas verificacoes_email/cadastro_rate_limit — usadas pelo código, aplicadas em produção por fora deste arquivo, mas ausentes dele; sem isso uma instalação nova quebrava no primeiro cadastro/login). Testado ponta a ponta num stack Docker isolado (projeto/porta/volume próprios, produção nunca tocada): instalação do zero validando o init.sql corrigido; as 3 anomalias disparando via formulário real (consumo caindo de 30 para 15 km/l, preço 60% acima da média, odômetro voltando); bloco no painel e "marcar como lido" (incluindo CSRF ausente e alerta de outro dono); replay de 3 requisições com o mesmo client_uuid via api/registro.php confirmando 1 registro + 1 alerta só (não 3); cron rodando duas vezes seguidas confirmando dedupe do push; cadastro com e-mail novo vs. e-mail já existente comparados (mesmo HTTP 200, mesmo corpo, tempo equivalente); e descoberta + correção de um bug real nesse mesmo processo (o caminho de sucesso original fazia redirect 302 enquanto o de e-mail existente respondia 200 — diferença mais óbvia que timing, corrigida antes de fechar a versão) |
| 1.13.0 | 2026-07-05 | Notificações push (Web Push) dos lembretes: push_inscrever.php/push_desinscrever.php guardam a inscrição do navegador (push_inscricoes, deduplicada por hash do endpoint); sw.php ganha os handlers push/notificationclick; novo serviço cron (mesma imagem do web, sem HTTP) roda cron/enviar_lembretes_push.php de hora em hora, manda um push por lembrete vencido/próximo (uma vez só por lembrete, via lembretes.push_notificado_em) e some sozinho com inscrições expiradas que o push service reportar. Primeira dependência via Composer do projeto (minishlink/web-push, MIT) — VAPID + criptografia do payload são coisa demais pra reinventar na mão; src/vendor/ fica de fora do Git (scripts/composer-install.sh gera via Docker, sem exigir Composer no host). Toggle em Minha Conta só aparece com VAPID configurado. Testado ponta a ponta: geração de chaves VAPID na imagem da própria app, biblioteca carregando via autoload, e a função de envio/limpeza de inscrição expirada revisada linha a linha (sem servidor de push real disponível no ambiente de teste pra disparar uma notificação de verdade) |
| 1.12.0 | 2026-07-05 | Conquistas (gamificação) no painel principal: sequência de meses seguidos com registro, e 5 selos por marco (Primeira Carga, 10 Abastecimentos, Motorista Veterano, Economia do Mês, Manutenção em Dia), com progresso pro próximo selo. Tudo calculado na hora (calcularConquistas() em functions.php) a partir de registros/lembretes que já existiam — sem tabela nova, sem estado próprio pra desatualizar. Testado com dados sintéticos num MySQL isolado e descartável (sequência normal, sequência quebrada por mês sem registro, lembrete vencido derrubando o selo "Em Dia") — nenhum dado real de usuário foi lido ou alterado |
| 1.11.0 | 2026-07-05 | Autonomia estimada do tanque no painel principal: tanque_litros do veículo (já existia no cadastro, nunca usado em cálculo nenhum) multiplicado pela última média de consumo (calcularUltimaMedia()). Só exibido quando o veículo em contexto é inequívoco (filtro de veículo ativo, ou usuário com um único veículo cadastrado) e há tanto tanque quanto consumo calculável. Testado em produção com dados temporários (tanque + segundo abastecimento no mesmo veículo), removidos ao final |
| 1.10.1 | 2026-07-05 | Auditoria de segurança completa (leitura de todo o código-fonte): (1) CSV Injection — nome de veículo/descrição iam crus no CSV exportado em Relatórios; nova sanitizarCelulaCsv() em functions.php prefixa com aspas simples qualquer célula que comece com = + - @ tab CR, neutralizando fórmula/DDE no Excel/LibreOffice; (2) credential stuffing — bloqueio existente (tentativas_falhas) protegia só uma conta por vez; nova tabela login_rate_limit (mesmo padrão de cadastro_rate_limit) limita a 30 tentativas falhas/hora por IP em login.php; (3) vazamento de e-mail por timing — em esqueci_senha.php, o caminho "conta existe" (gera token + SMTP real) demorava visivelmente mais que "não existe" (só um SELECT), driblando a mensagem genérica; atraso artificial de 300–700ms equaliza os dois; (4) supply-chain — Bootstrap/Bootstrap Icons/Chart.js carregados do jsDelivr sem verificação; adicionado SRI (hash sha384 calculado do conteúdo real, verificado sem quebrar nada) + crossorigin="anonymous"; (5) HSTS ausente — docker/php/security.conf ganha Strict-Transport-Security, fechando a janela de downgrade na primeira visita antes do redirect do nginx (exigiu rebuild da imagem, security.conf não é bind-mount). IDOR, CSRF, SQLi (queries sempre parametrizadas, EMULATE_PREPARES off), sessão (regenerate_id, cookies Secure/HttpOnly/SameSite=Strict), hardening de Docker (read_only, cap_drop: ALL, DB sem porta exposta) e .env/segredos revisados — nenhum problema encontrado nessas frentes |
| 1.10.0 | 2026-07-05 | Quatro melhorias práticas sem tocar no schema: combustivel.php (nova página, calculadora Etanol × Gasolina com limiar ajustável, link no dropdown da conta e atalho no dashboard); relatorios.php ganha o card "Custo por Km" (grade 2×2 de stats) e o gráfico rosca "Para Onde Vai o Dinheiro" (nova query agrupando por categoria calculada — combustível/manutenção/categoria de despesa); nova função calcularEstatisticasVeiculo() em functions.php alimenta uma tabela de comparação entre veículos (consumo médio, custo/km, gasto no período) exibida só com 2+ veículos cadastrados; dashboard mostra projeção de gasto do mês (mantendo o ritmo diário atual) comparada com a meta, e Relatórios ganham atalhos "Este mês/Últimos 30 dias/Este ano" que preenchem as datas e enviam o filtro via JS. combustivel.php e seu JS entram em PAGINAS_AUTENTICADAS/PRECACHE_URLS do Service Worker. Testado com Playwright em produção: calculadora nos dois vereditos, gráfico de categorias renderizando, atalhos de período filtrando, ausência de erros de console |
| 1.9.0 | 2026-07-05 | Recuperação de senha: esqueci_senha.php (rate limit de 5/hora por IP, tabela redefinicao_rate_limit, sempre responde com a mesma mensagem genérica pra não revelar se o e-mail existe) gera um token de uso único (tabela redefinicoes_senha, só o hash é guardado, válido por 1h) e manda por e-mail um link pra redefinir_senha.php, que troca a senha (com lock via transação, igual ao aceite de convite) e zera bloqueio/tentativas falhas da conta. Tela de login ganhou checkbox "Lembrar meu e-mail" (guardado só no localStorage do aparelho, nunca a senha) e botão de olho pra mostrar/ocultar senha (assets/js/auth.js), replicado no cadastro e na redefinição. Testado com Playwright: geração do token em produção, página de link inválido/expirado, toggle de senha e ausência de erros de console |
| 1.8.0 | 2026-07-05 | Meta de gasto mensal: nova coluna usuarios.meta_mensal, formulário em conta.php (ação salvar_meta, aceita vírgula ou ponto decimal, vazio remove a meta) e barra de progresso colorida no card de resumo do dashboard (index.php) comparando gasto do mês vs. meta — verde até 70%, amarelo até estourar, vermelho acima de 100%, com texto de quanto falta ou quanto passou. Corrigido também bug visual em instalar.php: a fingerprint SHA-256 do APK estourava a largura da tela em telas estreitas (sem quebra de linha) — nova classe .fingerprint-apk (word-break: break-all) e overflow-x: hidden no body como rede de segurança. Testado com Playwright: login real, ajuste da meta nos três estados (ok/atenção/estourada) e remoção, e scrollWidth/clientWidth de instalar.php confirmando ausência de overflow |
| 1.7.0 | 2026-07-01 | Cadastro inteligente de veículo: novos campos cor/placa, e busca de modelo (api/buscar_modelo.php, separa texto de ano automaticamente — ex. "Bros 160 2025") que autopreenche tanque/peso a partir de um novo catálogo (modelos_veiculos, ~20 modelos comuns no Brasil). Relatórios ganham card de comparação do consumo real com o de fábrica (cidade/estrada) quando o veículo tem modelo vinculado. Testado com Playwright: busca retornando o modelo certo, autopreenchimento, salvamento no banco e card de comparação renderizando com os números certos |
| 1.6.9 | 2026-07-01 | Removido o layout separado de desktop/notebook (sidebar, colunas largas) — o app é mobile-only de propósito agora, mesma disposição em qualquer tamanho de tela, só centralizada numa largura de celular a partir de 560px. assets/js/viewport.js removido (órfão, só existia pra decidir esse layout) |
| 1.6.8 | 2026-07-01 | Duas correções: (1) offline logo após instalar o app do zero — o cache de páginas autenticadas só era preenchido no install (que roda antes do primeiro login) ou visitando cada tela manualmente; agora offline.js avisa o Service Worker via postMessage assim que confirma sessão ativa, recarregando o cache sem precisar navegar por tudo primeiro; (2) espaço vazio grande sobrando embaixo da tela em páginas com pouco conteúdo (ex.: nenhum registro ainda) — body/.app-shell/main.container viram uma coluna flex de altura real (100dvh), e o bloco de lista cresce pra preencher o espaço, centralizando só o estado vazio (listas com dados continuam alinhadas no topo). Ambas testadas com Playwright (instalação do zero + bloqueio de rede real, e inspeção do layout renderizado) |
| 1.6.7 | 2026-07-01 | Duas correções sérias do modo offline: (1) o install do Service Worker agora busca cada página autenticada com a sessão atual e só cacheia se vier direto (sem redirect pra login) — antes, cada atualização de versão apagava o cache antigo e deixava tudo vazio até o usuário visitar cada página manualmente; (2) offline.js não confia mais só em navigator.onLine (que reporta "online" mesmo com wifi/dados sem internet de verdade) — testa uma busca real (HEAD /manifest.json, 2.5s) antes de deixar o formulário submeter nativamente, evitando o app travar no aviso de "confirmar reenvio do formulário" do navegador. Fallback final do SW também trocado por uma resposta com a marca do app em vez da tela genérica do navegador. Testado com Playwright com bloqueio de rede real (route.abort) |
| 1.6.6 | 2026-07-01 | Correção de texto/elementos renderizando maiores dentro do app instalado (TWA) do que numa aba comum do navegador: faltava text-size-adjust: 100% no CSS, e sem isso o WebView do Android aplica sozinho um recurso de "aumentar a fonte pra ficar legível" (pensado pra sites antigos não responsivos) — trava em 100% pra sempre respeitar o tamanho definido no CSS |
| 1.6.5 | 2026-07-01 | Correção definitiva do layout "de PC" no celular (sidebar/colunas largas): a tentativa anterior (1.6.2) travava por @media (pointer: fine), mas essa media feature dá falso positivo em aparelhos com caneta (ex.: S Pen em celulares Samsung) mesmo em uso 100% por toque, porque ela vale "verdadeiro" se QUALQUER mecanismo de entrada disponível for de precisão, não só o principal. Novo assets/js/viewport.js usa navigator.maxTouchPoints (a única checagem confiável de tela de toque) pra decidir, via JS, se marca a classe is-desktop-real no <html> — o CSS do layout desktop agora exige essa classe em vez de confiar só na media query. Testado com Playwright simulando os dois cenários (toque+pointer:fine falso-positivo vs. desktop real sem toque) |
| 1.6.4 | 2026-07-01 | Correção crítica do modo offline: o Service Worker registrava (e pré-cacheava páginas autenticadas) já na tela de login, antes do usuário logar — como index.php/adicionar.php/etc. redirecionam pra login.php sem sessão, o fetch() seguia o redirect e guardava a tela de login no cache com a chave da página original, fazendo o app parecer "quebrado" offline. Páginas autenticadas saíram da pré-carga (só é cacheada sob demanda, quando visitada online já logado) e o handler de navegação passou a ignorar explicitamente qualquer resposta que veio de um redirect pra login.php |
| 1.6.3 | 2026-07-01 | Auto-reload quando o Service Worker troca de versão (controllerchange): antes, uma aba já aberta continuava presa na versão antiga até fechar tudo manualmente — agora correções futuras chegam sozinhas, sem intervenção do usuário |
| 1.6.2 | 2026-07-01 | Correção do layout "de PC" surgindo em celulares/PWA/APK: o breakpoint de sidebar (≥992px) passou a exigir pointer: fine (mouse), já que alguns WebViews reportam a largura de viewport errada pro CSS num toque; o .container também trava em 100% de largura fora do modo desktop-com-mouse como segunda camada de proteção |
| 1.6.1 | 2026-07-01 | Correção de bug crítico do modo offline: faltava connect-src no CSP, então o fetch() do Service Worker pro CDN (Bootstrap/ícones) era bloqueado depois do 1º login, derrubando a formatação de todo o app; grandfather de contas anteriores à confirmação de e-mail e ajuste do padding-bottom da bottom-nav vazando nas telas de login/cadastro |
| 1.6.0 | 2026-07-01 | Modo offline completo (Service Worker + fila IndexedDB com sincronização automática/Background Sync, API idempotente por client_uuid), reabertura do cadastro público com confirmação de e-mail por código de 6 dígitos (rate limit por IP), Painel Administrativo com dados agregados por conta (papel admin) e aviso de atualização com changelog simplificado nas primeiras aberturas após uma nova versão |
| 1.5.0 | 2026-07-01 | Categoria "Despesa" no registro (Seguro, IPVA, Estacionamento, Pedágio, Multa, Lavagem, Outro), lembretes de manutenção/documentos por km ou por data com alerta no painel principal, filtro de relatórios por período (data início/fim) e exportação em CSV ou PDF (impressão do navegador) |
| 1.4.0 | 2026-07-01 | Redesign visual com base em pesquisa de apps reais da categoria (Drivvo): correção definitiva do bug do botão de novo registro sobrepondo valores da lista (agora embutido na barra de navegação, não mais flutuante), paleta com duas cores (laranja + teal) e selos de ícone nas estatísticas, estados vazios com ícone/texto/CTA, sidebar de navegação compacta e grade de 2 colunas nos gráficos para telas ≥992px, barras dos gráficos com largura proporcional e emojis trocados por ícones Bootstrap Icons na página de instalação |
| 1.3.0 | 2026-06-30 | Redesign visual (cantos suaves, sombras com tom da marca, transições de toque/hover, entrada animada de página/listas, medidor (gauge) SVG animado com contagem progressiva do km/l, respeitando prefers-reduced-motion), app Android nativo (TWA assinado via Bubblewrap) com APK para download, Digital Asset Links (abre em tela cheia sem barra de URL) e página pública /instalar.php com instruções APK/PWA |
| 1.2.0 | 2026-06-30 | Registro por convite (token único por e-mail, SMTP próprio sem dependências), conformidade LGPD (política de privacidade, consentimento, exclusão de conta), combustível no abastecimento (Gasolina Comum/Aditivada, Etanol, Diesel, GNV, Outro), preço por litro calculado, página de Relatórios com gráficos (gasto por mês, km rodado, evolução do consumo) e reorganização da navegação (dropdown de conta + bottom-nav com Relatórios) |
| 1.1.0 | 2026-06-30 | Multi-usuário: cadastro/login/logout com lockout de tentativas, isolamento de dados por conta (correção de IDOR em exclusão/edição), edição de veículo e registro, identidade visual própria (logo, paleta, favicons) e manifest PWA |
| 1.0.0 | 2026-06-30 | Versão inicial: CRUD de veículos/registros, cálculo de km/l, hardening completo, deploy em produção com Nginx + Let's Encrypt em pitstop.morenadoaco.com.br |




