An office task tracker that scrapes email, passes each message to an LLM agent that classifies it into a Person, Task, and Summary, stores the result, and displays it in a web frontend.
| Layer | Tech |
|---|---|
| Frontend | Astro 5 + Vite + pnpm |
| Backend | Python / FastAPI, managed with uv |
| Storage | PostgreSQL 16 |
| Scheduler | APScheduler (background mail polling) |
| Agent | Provider-agnostic LLM via LiteLLM |
┌────────────┐ /api ┌────────────┐ poll ┌──────────┐
Email ─►│ frontend │ ◄───────► │ backend │ ◄────────► │ postgres │
IMAP └────────────┘ │ (FastAPI) │ └──────────┘
│ └─ scrape → agent → store │
└────────────┘
.
├── .env.example # single source of truth for ALL env vars
├── docker-compose.yml # dev + prod profiles
├── backend/ # Python (uv) FastAPI app
│ ├── pyproject.toml # dependencies + tool config
│ ├── Dockerfile # dev / prod targets
│ └── app/
│ ├── main.py # app entrypoint, lifespan, router mounting
│ ├── config.py # pydantic-settings reading .env
│ ├── db.py # async SQLAlchemy engine / session
│ ├── models.py # Person, Task, Summary ORM models
│ ├── schemas.py # Pydantic API schemas
│ ├── jobs.py # APScheduler mail-polling job
│ ├── routers/tasks.py
│ └── services/
│ ├── mail.py # IMAP scraping (stub)
│ └── agent.py # LLM classification (stub)
└── frontend/ # Astro (pnpm + Vite)
├── astro.config.mjs # node adapter + /api dev proxy
├── Dockerfile # dev / build / prod targets
└── src/
├── pages/index.astro
├── layouts/Layout.astro
└── components/TaskList.astro
Everything is driven by a single root .env file. Docker Compose passes it to every
service via env_file, so there is no per-service duplication.
cp .env.example .envOpen .env and fill in at least:
POSTGRES_PASSWORD— database passwordIMAP_HOST,IMAP_USER,IMAP_PASSWORD— mailbox for scrapingLITELLM_MODEL+ a provider key (OPENAI_API_KEY,ANTHROPIC_API_KEY, …) for the agent
PUBLIC_API_URLis inlined into the client bundle at build time. In dev it defaults to/apiand the Vite server proxies to the backend automatically. In prod set it to the externally reachable backend URL.
- Docker with Compose v2
- uv (local dev / lockfile generation)
- Node.js ≥ 22 and pnpm (via
corepack enable)
Development profile — hot reload, ports exposed:
docker compose --profile dev up --buildBuild networking note: compose builds use
network: hostsouv/pnpmcan download dependencies when the default bridge network has no outbound internet. If your Docker daemon already has build-network access, you can remove thesenetwork: hostlines.
- Frontend: http://localhost:4321
- Backend API + Swagger UI: http://localhost:8000/docs
Production profile — built images, production servers:
docker compose --profile prod up -d --buildStop / clean up:
docker compose down # stop containers (keeps DB data)
docker compose down -v # stop + delete the postgres volumeRun the database, then the app from your host:
# terminal 1: postgres
docker compose --profile dev up postgres
# terminal 2: backend
cd backend
uv sync
uv run uvicorn app.main:app --reload --port 8000
# terminal 3: frontend
cd frontend
corepack enable && pnpm install
pnpm devThe MAIL_INGESTION_MODE env var controls how emails are scraped:
| Value | Behaviour |
|---|---|
background |
APScheduler polls the inbox every MAIL_POLL_SECONDS seconds |
manual |
Nothing runs automatically; call POST /api/tasks/ingest |
Manual trigger example:
curl -X POST http://localhost:8000/api/tasks/ingest -H 'Content-Type: application/json' \
-d '{"limit": 25}'| Method | Endpoint | Description |
|---|---|---|
| GET | /api/tasks |
List classified tasks (newest first) |
| GET | /api/tasks/{id} |
Fetch a single task |
| POST | /api/tasks/ingest |
Manually scrape + classify emails |
This is an initial boilerplate commit. The end-to-end pipeline (app, models, API, Docker,
env wiring) is in place, but the two core pieces are intentionally stubbed and live under
backend/app/services/:
mail.py→scrape_emails()— add the real IMAP fetch logic.agent.py→classify_email()— add the LiteLLM call that extracts Person / Task / Summary.
Once implemented, the background job (or manual trigger) will persist classified tasks, and the
frontend TaskList will display them.