Files
Jonathan 486749a890 Initial project scaffold
Full-stack Dutch supermarket price tracker with FastAPI backend,
PostgreSQL/SQLAlchemy, Albert Heijn scraper, and Next.js frontend.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 22:27:24 +02:00

138 lines
4.0 KiB
Markdown

# 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 <repo> && 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/<store_slug>.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)