Add BeanFlows MVP: coffee analytics dashboard, API, and web app

- Fix pipeline granularity: add market_year to cleaned/serving SQL models
- Add DuckDB data access layer with async query functions (analytics.py)
- Build Chart.js dashboard: supply/demand, STU ratio, top producers, YoY table
- Add country comparison page with multi-select picker
- Replace items CRUD with read-only commodity API (list, metrics, countries, CSV)
- Configure BeanFlows plan tiers (Free/Starter/Pro) with feature gating
- Rewrite public pages for coffee market intelligence positioning
- Remove boilerplate items schema, update health check for DuckDB
- Add test suite: 139 tests passing (dashboard, API, billing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-18 16:11:50 +01:00
parent b222c01828
commit 2748c606e9
59 changed files with 6272 additions and 2 deletions

78
web/CLAUDE.md Normal file
View File

@@ -0,0 +1,78 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What This Is
A **Copier template** for generating SaaS applications. The template lives in `{{project_slug}}/` with Jinja2-templated files (`.jinja` extension). Files without `.jinja` are copied as-is. The `copier.yml` defines template variables: `project_slug`, `project_name`, `description`, `author_name`, `author_email`, `base_url`, `payment_provider` (stripe/paddle/lemonsqueezy).
This is NOT a runnable application itself — it generates one via `copier copy`.
## Generated Project Commands
After generation, the project uses **uv** as package manager:
```bash
uv sync # Install dependencies
uv run python -m <slug>.migrations.migrate # Initialize/migrate DB
uv run python -m <slug>.app # Run dev server (port 5000)
uv run python -m <slug>.worker # Run background worker
uv run python -m <slug>.worker scheduler # Run periodic scheduler
uv run pytest # Run tests
uv run ruff check . # Lint
docker compose up -d # Production deploy
```
## Stack
- **Quart** (async Flask) with Jinja2 templates and Pico CSS (no build step)
- **SQLite** with WAL mode + aiosqlite (no ORM — plain SQL everywhere)
- **Stripe**, **Paddle**, or **LemonSqueezy** for billing (chosen via `payment_provider` template variable)
- **Resend** for transactional email
- **Litestream** for SQLite replication/backups
- **Docker + Caddy** for deployment
- **Hypercorn** as ASGI server in production
## Architecture
### Domain-based flat structure
Each domain is a directory with `routes.py` + `templates/`. Routes, SQL queries, and decorators all live together in `routes.py` — no separate models/services/repositories layers.
```
src/<slug>/
app.py → Application factory, blueprint registration, middleware
core.py → Config class, DB helpers (fetch_one/fetch_all/execute), email, CSRF, rate limiting
worker.py → SQLite-based background task queue (no Redis)
auth/ → Magic link auth, login_required/subscription_required decorators
billing/ → Checkout/webhooks/portal (Stripe, Paddle, or LemonSqueezy), plan feature/limit checks
dashboard/ → User settings, API key management
public/ → Marketing pages (landing, terms, privacy)
api/ → REST API with Bearer token auth (api_key_required decorator)
admin/ → Password-protected admin panel (ADMIN_PASSWORD env var)
migrations/ → schema.sql + migrate.py (runs full schema idempotently with CREATE IF NOT EXISTS)
```
### Key patterns
- **Database access**: Use `fetch_one()`, `fetch_all()`, `execute()` from `core.py`. No ORM. Queries live directly in route files next to the routes that use them.
- **Auth decorators**: `@login_required` for user pages, `@subscription_required(plans=["pro"])` for plan-gated features, `@api_key_required(scopes=["read"])` for API endpoints.
- **CSRF**: `@csrf_protect` decorator on POST routes. Templates include `<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">`.
- **Background tasks**: `from ..worker import enqueue` then `await enqueue("task_name", {"key": "value"})`. Register handlers with `@task("task_name")` decorator in `worker.py`.
- **Blueprints**: Each domain registers as a Quart Blueprint with its own `template_folder`. Templates in domain dirs override shared ones.
- **Soft deletes**: `deleted_at` column pattern. Use `soft_delete()`, `restore()`, `hard_delete()` from `core.py`.
- **Dates**: All stored as ISO 8601 TEXT in SQLite, using `datetime.utcnow().isoformat()`.
- **Rate limiting**: SQLite-based, no Redis. `@rate_limit()` decorator or `check_rate_limit()` function.
- **Migrations**: Single `schema.sql` file with all `CREATE TABLE IF NOT EXISTS`. Run via `python -m <slug>.migrations.migrate`.
### Conditional template blocks
`billing/routes.py.jinja` conditionally generates the full billing implementation based on the `payment_provider` variable (stripe/paddle/lemonsqueezy). The `.env.example.jinja`, `core.py.jinja`, and `schema.sql.jinja` similarly adapt per provider.
## Design Philosophy
Data-oriented, minimal abstraction. Plain SQL over ORM. Write code first, extract patterns only when repeated 3+ times. SQLite as the default database. Server-rendered HTML with Pico CSS.
## Ruff Config
Line length 100, target Python 3.11+, rules: E, F, I, UP (ignoring E501).