# Dutch Food Price Tracker Track Dutch supermarket food prices over time. Currently supports Albert Heijn. ## Stack | Layer | Tech | |---|---| | Backend API | Python FastAPI + SQLAlchemy + Alembic | | Database | PostgreSQL 16 | | Scraper | httpx + AH anonymous token auth | | Frontend | Next.js 14 + TypeScript + Tailwind CSS + Recharts | | Dev infra | Docker Compose | ## Quick start (Docker) ```bash git clone && cd dutch-food-price-tracker # Build and start all services docker compose up --build -d # Run DB migrations (tables are also auto-created on backend start) docker compose exec backend alembic upgrade head # Scrape Albert Heijn docker compose exec backend python cli.py scrape-ah \ --query melk \ --query brood \ --query kaas \ --query yoghurt ``` - Frontend: http://localhost:3000 - API docs: http://localhost:8000/docs - Database: `localhost:5432` (user: postgres / password: postgres / db: food_prices) ## Local development (without Docker) ### Backend ```bash cd backend python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -r requirements.txt # Needs a running PostgreSQL instance export DATABASE_URL=postgresql://postgres:postgres@localhost:5432/food_prices # Create tables alembic upgrade head # or: python -c "from app.database import engine; from app.models import Base; Base.metadata.create_all(engine)" # Start API server uvicorn app.main:app --reload ``` ### Frontend ```bash cd frontend npm install # Point at the local backend echo "NEXT_PUBLIC_API_URL=http://localhost:8000" > .env.local npm run dev ``` ## CLI reference ```bash # Scrape one or more queries python cli.py scrape-ah --query "melk" --query "brood" # Run via Docker docker compose exec backend python cli.py scrape-ah --query melk ``` ## API endpoints | Method | Path | Description | |---|---|---| | GET | `/api/products?search=melk` | Search products | | GET | `/api/products/{id}` | Product detail | | GET | `/api/products/{id}/prices` | Full price history | | GET | `/api/prices/cheapest?date=2024-01-15` | Cheapest per product for a day | | GET | `/api/stores` | List stores | | GET | `/api/scrape-runs` | List recent scrape runs | ## Project structure ``` dutch-food-price-tracker/ ├── backend/ │ ├── app/ │ │ ├── main.py # FastAPI app + CORS │ │ ├── models.py # SQLAlchemy ORM models │ │ ├── schemas.py # Pydantic request/response schemas │ │ ├── database.py # Engine, session, Base │ │ ├── config.py # Pydantic settings │ │ ├── routers/ # products, stores, prices, scrape_runs │ │ └── scrapers/ │ │ └── albert_heijn.py # Token auth + product search │ ├── alembic/ # DB migration history │ ├── cli.py # Click CLI (scrape-ah) │ └── requirements.txt ├── frontend/ │ └── src/ │ ├── app/ │ │ ├── page.tsx # Product search │ │ ├── products/[id]/page.tsx # Detail + price chart │ │ └── cheapest/page.tsx # Daily cheapest overview │ ├── components/ │ │ ├── Nav.tsx │ │ ├── ProductCard.tsx │ │ └── PriceChart.tsx # Recharts line chart │ └── lib/api.ts # Typed API client ├── seed/ # Historical CSV/JSON import datasets ├── docker-compose.yml └── .env.example ``` ## Adding a new store 1. Create `backend/app/scrapers/.py` 2. Implement `scrape_query(db: Session, query: str) -> ScrapeRun` 3. Add a `@cli.command` in `backend/cli.py` 4. Insert a `Store` row with the new slug ## Data model - **stores** — one row per chain (Albert Heijn, Jumbo, …) - **products** — one row per store SKU; keyed by `(store_id, external_id)` - **scrape_runs** — one row per CLI invocation / query - **price_snapshots** — append-only price observations (cents, UTC timestamp)