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.
Este projeto usa a Business Source License 1.1 (BSL), a mesma licença adotada por MariaDB,
Sentry e CockroachDB — não é uma licença OSI "open source" tradicional, é "source-available": o
código fica público e qualquer um pode ler, rodar e modificar pra uso pessoal, educacional ou
interno, mas não pode oferecer o PitStop BR (ou uma versão modificada dele) como produto/serviço
hospedado competindo comercialmente, nem revendê-lo, sem uma licença comercial separada do autor.
A partir de 12/07/2030 (4 anos após a mudança), o código vira automaticamente MIT — totalmente
livre, sem restrição nenhuma. Texto completo em LICENSE; pra licenciamento comercial
antes dessa data, contato em alexandre.basto444@gmail.com.
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.17.2 | 2026-07-11 | Novo "Histórico de Abastecimentos" em relatorios.php: lista os últimos 50 abastecimentos (data, km, litros, valor) filtrados pelos mesmos filtros de veículo/período da página, com um selo verde/amarelo indicando se cada um foi tanque cheio ou complemento parcial. Diferente do gráfico "Evolução do Consumo" (que só mostra dado quando calcularTrechosConsumo() fecha um trecho entre dois tanques cheios), essa lista é o dado cru — sempre aparece, mesmo sem nenhum trecho fechado ainda |
| 1.17.1 | 2026-07-11 | Relatórios, segunda leva: novo card "Consumo Médio" no topo (6 cards, grid 2×3 balanceado) — só quando o veículo é inequívoco (filtro ativo ou usuário com 1 só), mesmo critério da autonomia do dashboard. Doughnut "Para Onde Vai o Dinheiro" ganha o total formatado no centro (plugin inline no Chart.js, sem lib nova). Gráfico "Evolução do Consumo" passa a desenhar o consumo de fábrica (cidade/estrada) como linhas de referência tracejadas quando o veículo tem modelo vinculado, com legenda (2+ séries). Tabela de Comparação entre Veículos ganha destaque (cor + troféu) do melhor consumo e menor custo/km por linha, e uma linha de Km Rodado que faltava |
| 1.17.0 | 2026-07-11 | Conformidade LGPD: privacidade.php reescrita por completo (base legal explícita, compartilhamento via push/CDN detalhado, retenção de dados técnicos, seção de cookies, histórico de versões da própria política). Novo api/exportar_dados.php (link em Minha Conta) — portabilidade de dados (Art. 18, V), baixa JSON com perfil/veículos/registros/lembretes do usuário, nunca inclui senha_hash nem dado de segurança. Novo cron/limpar_dados_expirados.php (rodando no loop horário do serviço cron) apaga *_rate_limit com mais de 48h e verificacoes_email/redefinicoes_senha com mais de 2 dias — minimização de retenção (Art. 6º, III); não mexe em convites (aparece como histórico pro usuário em convidar.php). Aviso de cookies informativo no primeiro acesso (assets/js/cookies.js, localStorage) — cookie de sessão é estritamente necessário, então é só informativo, não um opt-in |
| 1.16.2 | 2026-07-11 | Melhorias de segurança: convidar.php não revela mais se um e-mail já tem conta e ganhou rate limit (convite_rate_limit, por usuário); sessões ativas são revogadas quando a senha é trocada (usuarios.sessao_valida_apos, checado em todo request via checarRevogacaoDeSessao()); código de verificação de e-mail passa de hash simples para password_hash() (bcrypt), mesmo padrão da senha; idempotência de client_uuid (fila offline) escopada por veículo em vez de global (UNIQUE KEY composta em registros/lembretes) |
| 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 |




