From 869800d9c4f2435c08dd606df7e81f321bb2c06a Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sat, 10 Oct 2026 04:20:33 -0300 Subject: [PATCH] =?UTF-8?q?docs(readme):=20documentar=20testes,=20CI=20e?= =?UTF-8?q?=20estado=20atual=20do=20reposit=C3=B3rio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 64 ++++++++++++++++++++----------------------------------- 1 file changed, 23 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 86eb376..f5c3c46 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ **Consulta nutricional de produtos de supermercado: busca por nome ou código de barras na base colaborativa do Open Food Facts, mostra Nutri-Score e grau de processamento (NOVA) e organiza listas de compras, com cache local para continuar funcionando sem internet.** +![CI](https://github.com/evandrodevbr/nutriscan/actions/workflows/ci.yml/badge.svg) ![Next.js](https://img.shields.io/badge/Next.js-15-black?logo=nextdotjs&logoColor=white) ![React](https://img.shields.io/badge/React-19-61DAFB?logo=react&logoColor=black) ![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?logo=typescript&logoColor=white) @@ -61,6 +62,7 @@ cache do cliente: resultados de busca e filtros no localStorage (TTL configuráv - Node.js `>=18.18` (o deploy fixa Node 18; o build foi verificado em Node 24.20.0) - pnpm 10 ou superior (o deploy fixa 10.34.5; a instalação foi verificada com pnpm 12.4.1 e a resolução de dependências foi verificada com pnpm 10.34.5) +- Para rodar os testes: Node.js `>=22.15` (verificado em 26.7.0) - Nenhum serviço externo é obrigatório para subir a aplicação: sem internet ela responde pelo cache local ## Início rápido @@ -107,7 +109,7 @@ Todas as rotas ficam em `app/api` e são Route Handlers do Next.js. | `/api/search` | GET | Busca produtos no cache JSON e, se faltar, no Open Food Facts | | `/api/products/[barcode]` | GET | Busca um produto pelo código de barras (cache primeiro, depois API) | | `/api/location` | GET | País estimado a partir do IP (`x-forwarded-for`, `x-real-ip` ou api.ipify.org) | -| `/api/cache/sync` | POST | Grava um lote de produtos no cache JSON. Corpo: `{ "products": [...] }` | +| `/api/cache/sync` | POST | Grava um lote de produtos no cache JSON. Corpo: `{ "products": [...] }`; itens sem `code` numérico são ignorados e contados em `failed` | | `/api/storage-stats` | GET | Total de produtos, tamanho do arquivo e data da última atualização do cache | Parâmetros de `/api/search`: @@ -179,6 +181,7 @@ hooks/ busca, filtros, cache, geolocalização e monitor d lib/ ├── openFoodFactsApi.ts cliente da API do Open Food Facts e ordenação ├── jsonCacheManager.ts cache JSON do servidor (data/products-cache.json) +├── offSearch.ts montagem e paginação das buscas no Open Food Facts ├── cacheManager.ts cache de buscas no localStorage ├── geolocation.ts detecção de país do usuário ├── searchUtils.ts ordenação por relevância @@ -186,53 +189,36 @@ lib/ └── prisma.ts singleton do Prisma Client components/ui/ componentes shadcn/ui (button, dialog, select, sheet, ...) prisma/schema.prisma modelos Lista e Produtos +tests/ testes (node:test) e helper de resolução de módulos +.github/workflows/ci.yml CI: lint, typecheck, testes e build data/products-cache.json cache inicial com 232 produtos docs/ documentação interna nixpacks.toml, dokploy.yaml configuração de deploy ``` -## Verificação +## Testes e desenvolvimento -Há testes offline com o runner nativo do Node.js (sem framework adicional) e não há CI configurada. O que existe hoje: - -- `pnpm lint` roda o ESLint (via `next lint`); -- `pnpm build` roda `prisma generate` e o `next build` com checagem de tipos; -- verificação manual de rotas com `curl`, descrita abaixo. - -Medições feitas nesta auditoria, em 14/09/2026, com Node 24.20.0 e pnpm 12.4.1: - -| Comando / requisição | Resultado | +| Comando | O que faz | |---|---| -| `pnpm install` | exit 0, sem precisar de flags | -| `pnpm lint` | exit 0, 6 avisos de variável não usada | -| `pnpm build` | exit 0, 13 páginas geradas, First Load JS compartilhado de 102 kB | -| `pnpm start -p 3111` | servidor no ar, respondeu 200 em 1 s | -| `GET /` | 200, 46 kB | -| `GET /sobre`, `/resultados`, `/robots.txt`, `/sitemap.xml`, `/manifest.json` | 200 | -| `GET /produto/3017620422003` | 200, produto Nutella obtido na hora na API | -| `GET /lista/audit-teste` | 200, página de lista renderizada | -| `GET /api/search?q=bis&cache_only=true` | 200, 20 produtos vindos do cache | -| `GET /api/search?q=bis&cache_only=true&page_size=5` | 200, 5 produtos | -| `GET /api/products/7622210575975?cache_only=true` | 200, `fromCache: true` | -| `GET /api/products/3017620422003` | 200, `fromAPI: true` (produto buscado e gravado no cache) | -| `GET /api/storage-stats` | 200, `totalProducts: 232`, 0,55 MB | -| `GET /api/location` | 200, `{"country":"Brazil","countryCode":"br"}` | -| `POST /api/cache/sync` com `{"products":[]}` | 200, `{"success":true,"saved":0,"failed":0}` | -| `POST /api/cache/sync` com corpo inválido | 400, `{"error":"Produtos inválidos"}` | -| `GET /api/search` sem `q` | 400, `{"error":"Query parameter is required"}` | -| `GET /api/products/123` | 400, `{"error":"Código de barras inválido"}` | -| `GET /api/search?q=x&page=0` | 400, `{"error":"Invalid page parameter"}` | +| `pnpm test` | Testes com o runner nativo do Node (`node:test`), sem framework adicional | +| `pnpm lint` | ESLint via `next lint` | +| `pnpm typecheck` | `tsc --noEmit` | +| `pnpm build` | `prisma generate` + `next build` | + +Os testes não acessam a rede: `fetch` é substituído por um mock e o cache JSON do servidor aponta para uma pasta temporária, então `data/products-cache.json` não é alterado. Eles cobrem a normalização de energia e nutrientes, a ordenação, a montagem das requisições ao Open Food Facts (`lib/offSearch.ts`), as rotas `/api/search`, `/api/products/[barcode]` e `/api/cache/sync` e a gravação concorrente do cache. O código-fonte TypeScript é executado direto pelo Node (type stripping), por isso `pnpm test` exige Node.js 22.15 ou superior; o helper `tests/helpers/register.mjs` resolve o alias `@/` e os imports sem extensão. + +O workflow `.github/workflows/ci.yml` roda lint, typecheck, testes e build em cada push na `main` e em pull requests, com Node 22 e pnpm 10. + +Última execução local (Node 26.7.0, pnpm 12.8.1): `pnpm test`, `pnpm typecheck`, `pnpm lint` e `pnpm build` terminam com sucesso; `pnpm start -p 3111` respondeu 200 em `/`, `/lista/`, `/api/search?q=bis&cache_only=true&page_size=5`, `/api/storage-stats` e `/api/products/7622210575975?cache_only=true`, e 400 em `/api/search` sem `q`, `/api/products/123` e `POST /api/cache/sync` com corpo inválido. ## Estado atual e limitações -- A busca textual depende do endpoint legado `cgi/search.pl` do Open Food Facts, que respondeu HTTP 503 durante a verificação. Na prática, buscar por nome retorna lista vazia, com a rota respondendo 200 e `count: 0`, sem derrubar a aplicação. A consulta por código de barras continua funcionando pelo endpoint `api/v2/product`. -- Testes offline de normalização e ordenação nutricional; ainda sem CI. +- A busca textual depende do endpoint legado `cgi/search.pl` do Open Food Facts, que respondeu HTTP 503 em 14/09/2026. Na prática, buscar por nome retorna lista vazia, com a rota respondendo 200 e `count: 0`, sem derrubar a aplicação. A consulta por código de barras continua funcionando pelo endpoint `api/v2/product`. - O painel de filtros avançados da página de resultados guarda as escolhas na chave do cache, mas a busca enviada para `/api/search` leva apenas texto, país e paginação. Hoje o que realmente filtra a lista são os atalhos de classificação Nutri-Score e o botão de produtos sem alérgenos. - A ordenação implementada em `lib/openFoodFactsApi.ts` cobre quatro critérios (relevância, Nutri-Score, energia e nome). As demais opções do seletor caem no caso padrão da função e não reordenam o resultado. - O servidor grava em `data/products-cache.json`, que é versionado. Usar a aplicação altera o conteúdo do arquivo no diretório de trabalho. -- Duas lockfiles versionadas: `pnpm-lock.yaml` (usada pelo deploy) e `bun.lock`. -- Metadados, Open Graph e User-Agent apontam para `nutriscan.com.br`, domínio que não responde; a instância publicada está em `nutriscan.evandro.dev.br`, que também é a URL do `sitemap.xml` e do `robots.txt`. -- O banco SQLite versionado em `prisma/prisma/db/` vem só com a tabela `_prisma_migrations`, sem `Lista` e `Produtos`; é necessário rodar `pnpm db:push` antes de usar as listas. +- Metadados, Open Graph e User-Agent (`lib/offSearch.ts`, `lib/openFoodFactsApi.ts`, `app/api/location/route.ts`) apontam para `nutriscan.com.br`, domínio que não responde; a instância publicada está em `nutriscan.evandro.dev.br`, que também é a URL do `sitemap.xml` e do `robots.txt`. +- O repositório não traz banco SQLite: é necessário rodar `pnpm db:push` antes de usar as listas. - `pnpm db:migrate` existe no `package.json` mas não tem migrations para aplicar no repositório. - O analytics é servido via proxy em `/analytics/*` para o Rybbit configurado em `NEXT_PUBLIC_RYBBIT_HOST`; sem esse serviço no ar, o script falha silenciosamente no cliente. - A página de lista de compras renderiza para qualquer id, mesmo sem registro no banco, porque `verificarLista` retorna sucesso quando a lista não existe. @@ -252,10 +238,6 @@ Licença de uso pessoal, definida em [`LICENSE`](LICENSE): é permitido usar em Autor: Evandro, e [@evandrodevbr](https://github.com/evandrodevbr). -## Validação de dados nutricionais (2026-09-30) +## Normalização de energia e nutrientes -A exibição e a ordenação de energia priorizam `energy-kcal_100g`; na ausência dele, convertem o campo em kJ para kcal usando `1 kcal = 4,184 kJ`. A página do produto usa a mesma normalização. Zero é preservado como dado válido, e valores ausentes, negativos ou não finitos não entram no cálculo. Produtos sem energia ficam no fim da ordenação nas duas direções. A linha baseada em `salt_100g` é identificada como sal. - -Os exemplos de campos vêm da [documentação oficial do Open Food Facts](https://openfoodfacts.github.io/openfoodfacts-server/api/tutorial-off-api/). Valores por 100 g e referências existentes da interface são mantidos; o Nutri-Score continua vindo da API. - -Com Node.js 22.7+ (verificado em 24.19.0), `pnpm test` executa 6 regressões offline usando o runner nativo, sem adicionar um framework. `pnpm lint`, `pnpm exec tsc --noEmit` e `pnpm build` completam a validação. O build foi verificado com SQLite temporário, sem conexão com um banco de produção. Next.js e eslint-config-next foram atualizados para 15.5.24. +A exibição e a ordenação de energia priorizam `energy-kcal_100g`; na ausência dele, convertem o campo em kJ para kcal usando `1 kcal = 4,184 kJ`. A página do produto usa a mesma normalização. Zero é preservado como dado válido, e valores ausentes, negativos ou não finitos não entram no cálculo. Produtos sem energia ficam no fim da ordenação nas duas direções. A linha baseada em `salt_100g` é identificada como sal. A implementação está em `lib/nutrition.ts`; os campos seguem a [documentação do Open Food Facts](https://openfoodfacts.github.io/openfoodfacts-server/api/tutorial-off-api/).