Files
condado-newsletter/CLAUDE.md
Gabriel Sancho ebcea643c4 feat(backend): persist tasks and generated message history
- add EntityTask domain and CRUD API backed by PostgreSQL

- relate generated messages directly to tasks and delete on task removal

- move preview generation to backend Llama endpoint

- migrate frontend task APIs from localStorage to backend endpoints

- update tests and CLAUDE rules for backend-owned LLM/persistence
2026-03-27 02:46:56 -03:00

583 lines
29 KiB
Markdown

# Condado Abaixo da Média SA — Email Bot
A **monorepo** containing a Kotlin/Spring Boot backend and a React frontend. This file gives
the AI persistent instructions and context about the project so every session starts with the
right knowledge.
---
## Default Workflow: Test-Driven Development (TDD)
> **Every implementation step in this project follows TDD. This is non-negotiable.**
The cycle for every step is:
| Phase | Action | Gate |
|------------|-------------------------------------------------------------------------|------------------------------------|
| **Red** | Write test file(s) for the step. Run the test suite. | New tests must **fail**. |
| **Green** | Write the minimum implementation to make all tests pass. | All tests must **pass**. |
| **Refactor** | Clean up the implementation. | Tests must stay **green**. |
| **Done** | Mark step ✅ only when the full build is green. | `./gradlew build` / `npm run build && npm run test` |
**Rules:**
- Never write implementation code before the test file exists and the tests fail.
- Never mark a step Done unless the full test suite passes.
- Test method names (Kotlin/JUnit): `should_[expectedBehavior]_when_[condition]`.
- Backend mocking: **MockK** only (not Mockito).
- Backend integration tests: `@SpringBootTest` with **H2 in-memory** database.
- Frontend tests: **Vitest** + **React Testing Library**, mocked Axios.
---
## Project Overview
- **Type:** Monorepo (backend + frontend in the same repository)
- **Purpose:** Simulate virtual employees of the fictional company "Condado Abaixo da Média SA".
Each entity is an AI-powered fictional employee with a name, email address, job title,
personality, and an email schedule. At the scheduled time, the system reads recent emails
from the company mailbox (IMAP), builds a prompt, calls the OpenAI API, and dispatches the
AI-generated email via SMTP.
- **Tone rule (critical):** Every generated email must be written in an **extremely formal,
corporate tone** — but the **content is completely casual and nonsensical**, like internal
jokes between friends. This contrast is the core joke of the project and must be preserved
in every generated email.
- **Architecture:** React SPA → Spring Boot REST API → PostgreSQL + IMAP + SMTP + OpenAI
- **Deployment:** Fully containerized with Docker and Docker Compose
---
## Tech Stack
### Backend
| Layer | Technology |
|----------------|---------------------------------------------------|
| Language | Kotlin (JVM) |
| Framework | Spring Boot 3.x |
| Build Tool | Gradle (Kotlin DSL — `build.gradle.kts`) |
| Database | PostgreSQL (via Spring Data JPA) |
| Email Reading | Jakarta Mail (IMAP) — read inbox for context |
| Email Sending | Spring Mail (SMTP / JavaMailSender) |
| AI Integration | OpenAI API (`gpt-4o`) via Spring `RestClient` |
| Scheduler | Spring `@Scheduled` + `SchedulingConfigurer` |
| Auth | JWT — issued by backend on login |
| Testing | JUnit 5 + MockK |
| Docs | Springdoc OpenAPI (Swagger UI) |
### Frontend
| Layer | Technology |
|----------------|---------------------------------------------------|
| Language | TypeScript |
| Framework | React 18 + Vite |
| UI Library | shadcn/ui (Radix UI + Tailwind CSS) |
| State | React Query (TanStack Query v5) |
| Routing | React Router v6 |
| HTTP Client | Axios |
| Auth | JWT stored in `httpOnly` cookie (set by backend) |
| Testing | Vitest + React Testing Library |
### Infrastructure
| Layer | Technology |
|-------------------|--------------------------------------------------------------|
| Containers | Docker |
| Orchestration | Docker Compose — three flavours (see below) |
| Reverse Proxy | Nginx (serves frontend + proxies `/api` to backend) |
| Dev Mail | Mailhog (SMTP trap + web UI) |
| All-in-one image | Single Docker image: Nginx + Spring Boot + PostgreSQL + Supervisor |
| Image registry | Docker Hub (`<dockerhub-user>/condado-newsletter`) |
| CI/CD | GitHub Actions — build, test, push to Docker Hub on merge to `main` |
## Deployment Flavours
There are **three ways to run the project**:
| Flavour | Command | When to use |
|---------------------|---------------------------------|------------------------------------------------|
| **Dev** | `docker compose up` | Local development — includes Mailhog |
| **Prod (compose)** | `docker compose -f docker-compose.prod.yml up` | Production with external DB/SMTP |
| **All-in-one** | `docker run ...` | Simplest deploy — everything in one container |
### All-in-one Image
The all-in-one image (`Dockerfile.allinone`) bundles **everything** into a single container:
- **Nginx** — serves the React SPA and proxies `/api` to Spring Boot
- **Spring Boot** — the backend API + scheduler
- **PostgreSQL** — embedded database
- **Supervisor** — process manager that starts and supervises all three processes
This image is published to Docker Hub at `<dockerhub-user>/condado-newsletter:latest`.
**Minimal `docker run` command:**
```bash
docker run -d \
-p 80:80 \
-e APP_PASSWORD=yourpassword \
-e JWT_SECRET=yoursecret \
-e OPENAI_API_KEY=sk-... \
-e MAIL_HOST=smtp.example.com \
-e MAIL_PORT=587 \
-e MAIL_USERNAME=company@example.com \
-e MAIL_PASSWORD=secret \
-e IMAP_HOST=imap.example.com \
-e IMAP_PORT=993 \
-e APP_RECIPIENTS=friend1@example.com,friend2@example.com \
-v condado-data:/var/lib/postgresql/data \
<dockerhub-user>/condado-newsletter:latest
```
The app is then available at `http://localhost`.
---
## System Topology
### Multi-container (Docker Compose)
```
┌─────────────────────────────────────────────────────────────────┐
│ Docker Compose │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ nginx :80 / :443 │ │
│ │ • Serves React SPA (static files) │ │
│ │ • Proxies /api/** ──────────────────────────────────► │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ backend :8080 │ │
│ │ Spring Boot (Kotlin) │ │
│ │ • REST API /api/v1/** │ │
│ │ • JWT auth /api/auth/login │ │
│ │ • Swagger /swagger-ui.html │ │
│ │ • Scheduler (cron per VirtualEntity) │ │
│ └──────────┬───────────────┬──────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌────────────────────────────────────────┐ │
│ │ postgres :5432│ │ External services (outside Docker) │ │
│ │ PostgreSQL │ │ • OpenAI API (HTTPS) │ │
│ │ DB: condado │ │ • SMTP server (send emails) │ │
│ └──────────────┘ │ • IMAP server (read inbox) │ │
│ └────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ mailhog :1025 (SMTP) / :8025 (UI) │ │
│ │ DEV ONLY — catches outgoing emails │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Browser ──────► nginx :80
├── / → React SPA (index.html)
├── /assets → Static JS/CSS
└── /api/** → backend :8080
```
### All-in-one Image (single `docker run`)
```
┌──────────────────────────────────────────────────────────────┐
│ condado-newsletter :80 (single container) │
│ │
│ supervisord │
│ ├── nginx (port 80 — SPA + /api proxy) │
│ ├── java -jar app (Spring Boot :8080 — internal only) │
│ └── postgres (PostgreSQL :5432 — internal only) │
│ │
│ Persistent data → Docker volume mounted at │
│ /var/lib/postgresql/data │
│ │
│ External (via env vars): │
│ • OpenAI API (HTTPS) │
│ • SMTP server (send emails) │
│ • IMAP server (read inbox) │
└──────────────────────────────────────────────────────────────┘
```
**Data flows:**
1. User opens browser → Nginx serves the React app.
2. React calls `POST /api/auth/login` with password → backend validates against `APP_PASSWORD`
env var → returns JWT in `httpOnly` cookie.
3. React calls `/api/v1/virtual-entities` (JWT cookie sent automatically) → backend responds.
4. Scheduler ticks → for each due `VirtualEntity`:
- Backend reads IMAP inbox (external IMAP server).
- Builds prompt → calls OpenAI API.
- Sends generated email via SMTP (Mailhog in dev, real SMTP in prod).
- Saves `DispatchLog` to PostgreSQL.
---
## Monorepo Structure
```
condado-news-letter/ ← repo root
├── CLAUDE.md
├── INSTRUCTIONS.md
├── .env.example ← template for all env vars
├── .gitignore
├── docker-compose.yml ← dev stack (Nginx + Backend + PostgreSQL + Mailhog)
├── docker-compose.prod.yml ← prod stack (Nginx + Backend + PostgreSQL)
├── Dockerfile.allinone ← all-in-one image (Nginx + Backend + PostgreSQL + Supervisor)
├── .github/
│ └── workflows/
│ ├── ci.yml ← run tests on every PR
│ └── publish.yml ← build & push all-in-one image to Docker Hub on main merge
├── backend/ ← Spring Boot (Kotlin + Gradle)
│ ├── build.gradle.kts
│ ├── settings.gradle.kts
│ ├── Dockerfile
│ └── src/
│ ├── main/kotlin/com/condado/newsletter/
│ │ ├── CondadoApplication.kt
│ │ ├── config/
│ │ ├── controller/
│ │ ├── service/
│ │ │ ├── AuthService.kt
│ │ │ ├── EntityService.kt
│ │ │ ├── EmailReaderService.kt
│ │ │ ├── PromptBuilderService.kt
│ │ │ ├── AiService.kt
│ │ │ └── EmailSenderService.kt
│ │ ├── repository/
│ │ ├── model/
│ │ ├── dto/
│ │ └── scheduler/
│ └── main/resources/
│ ├── application.yml
│ └── application-dev.yml
├── frontend/ ← React (TypeScript + Vite)
│ ├── package.json
│ ├── vite.config.ts
│ ├── tsconfig.json
│ ├── Dockerfile
│ └── src/
│ ├── main.tsx
│ ├── App.tsx
│ ├── api/ ← Axios client + React Query hooks
│ ├── components/ ← Reusable UI components (shadcn/ui)
│ ├── pages/
│ │ ├── LoginPage.tsx
│ │ ├── DashboardPage.tsx
│ │ ├── EntitiesPage.tsx
│ │ └── LogsPage.tsx
│ └── router/ ← React Router config
└── nginx/
└── nginx.conf ← Shared Nginx config (used in both Docker flavours)
```
---
## Build & Run Commands
### Backend
```bash
cd backend
# Build the project
./gradlew build
# Run (dev profile)
./gradlew bootRun --args='--spring.profiles.active=dev'
# Run tests
./gradlew test
# Run a specific test
./gradlew test --tests "com.condado.newsletter.service.PromptBuilderServiceTest"
```
### Frontend
```bash
cd frontend
# Install dependencies
npm install
# Dev server (with Vite proxy to backend)
npm run dev
# Build for production
npm run build
# Run tests
npm run test
```
### Full Stack (Docker Compose)
```bash
# Dev (Mailhog included)
docker compose up --build
# Prod
docker compose -f docker-compose.prod.yml up --build
# Stop
docker compose down
```
---
## Coding Standards
### Project Language Policy
- **Primary language is English** for code, comments, commit messages, PR titles/descriptions, and user-facing UI text.
- Portuguese may be used only for informal chat outside code artifacts.
- When translating requirements to implementation, preserve meaning but keep identifiers and UI labels in English.
### Backend (Kotlin)
- Use **Kotlin idiomatic** style: data classes, extension functions, and null-safety operators.
- Prefer `val` over `var` wherever possible.
- Use **constructor injection** for dependencies (never field injection with `@Autowired`).
- All DTOs must be **data classes** with validation annotations (`jakarta.validation`).
- Controller methods must return `ResponseEntity<T>` with explicit HTTP status codes.
- Services must be annotated with `@Service` and **never** depend on controllers.
- Repositories must extend `JpaRepository<Entity, IdType>`.
- Use `@Transactional` on service methods that modify data.
- All public functions must have **KDoc** comments.
- Use **`snake_case`** for database columns and **`camelCase`** for Kotlin properties.
- Keep controllers thin — business logic belongs in services.
- The AI prompt construction logic must live **exclusively** in `PromptBuilderService`.
### Frontend (TypeScript / React)
- All components must be **functional** — no class components.
- Use **TypeScript strict mode** — no `any` types.
- All API calls go through the `src/api/` layer — never call `axios` directly in a component.
- Use **React Query** for all server state — never store server data in `useState`.
- Use **shadcn/ui** components for UI — do not build custom UI primitives from scratch.
- All pages live in `src/pages/` and are lazy-loaded via React Router.
- Protected routes must check for a valid JWT before rendering.
- No hardcoded strings that face the user — use constants or i18n keys.
---
## Authentication Model
- There is **one single user** — the admin (you and your friends sharing the app).
- The password is set via the `APP_PASSWORD` environment variable on the backend.
- `POST /api/auth/login` accepts `{ "password": "..." }` and returns a JWT in an `httpOnly`
cookie (no username needed).
- The JWT is validated by Spring Security on every protected backend request.
- The React frontend redirects to `/login` if the cookie is absent or the JWT is expired.
- There is **no user registration, no user table, no role system**.
---
## Naming Conventions
### Backend
| Artifact | Convention | Example |
|----------------|----------------------|-------------------------------|
| Classes | PascalCase | `PromptBuilderService` |
| Functions | camelCase | `buildPrompt()` |
| Variables | camelCase | `entityList` |
| Constants | SCREAMING_SNAKE_CASE | `MAX_EMAIL_CONTEXT_DAYS` |
| DB tables | snake_case (plural) | `virtual_entities` |
| REST endpoints | kebab-case | `/api/v1/virtual-entities` |
| Packages | lowercase | `com.condado.newsletter` |
### Frontend
| Artifact | Convention | Example |
|----------------|----------------------|-------------------------------|
| Components | PascalCase | `EntityCard.tsx` |
| Hooks | camelCase + `use` | `useEntities.ts` |
| API files | camelCase | `entitiesApi.ts` |
| Pages | PascalCase + `Page` | `EntitiesPage.tsx` |
| CSS classes | kebab-case (Tailwind)| `text-sm font-medium` |
---
## Testing Guidelines
> **This project follows strict TDD.** For every implementation step, tests are written
> first (Red), then the implementation is added until all tests pass (Green), then code is
> cleaned up (Refactor). Never write implementation code before the tests exist and fail.
### TDD Workflow (apply to every step)
1. **Red** — Write the test file(s). Run `./gradlew test` (backend) or `npm run test`
(frontend) and confirm the new tests **fail** (compile errors are acceptable at this stage).
2. **Green** — Write the minimum implementation needed to make all tests pass.
3. **Refactor** — Clean up the implementation while keeping tests green.
4. A step is only ✅ Done when `./gradlew build` (backend) or
`npm run build && npm run test` (frontend) is fully green.
### Backend
- Every service class must have a corresponding unit test class **written before the service**.
- Use **MockK** for mocking (not Mockito).
- Integration tests use `@SpringBootTest` and an **H2 in-memory** database.
- Test method names follow: `should_[expectedBehavior]_when_[condition]`.
- Minimum 80% code coverage for service classes.
- Test files live in `src/test/kotlin/` mirroring the `src/main/kotlin/` package structure.
### Frontend
- Every page component must have at least a smoke test (renders without crashing),
**written before the component**.
- API layer functions must be tested with mocked Axios responses.
- Use **Vitest** as the test runner and **React Testing Library** for component tests.
- Test files live in `src/__tests__/` mirroring the `src/` structure.
---
## Environment Variables
All variables are defined in `.env` (root of the monorepo) and injected by Docker Compose.
Never hardcode any of these values.
| Variable | Used by | Description |
|------------------------------|----------|------------------------------------------------------|
| `APP_PASSWORD` | Backend | Single admin password for login |
| `JWT_SECRET` | Backend | Secret key for signing/verifying JWTs |
| `JWT_EXPIRATION_MS` | Backend | JWT expiry in milliseconds (e.g. `86400000` = 1 day) |
| `SPRING_DATASOURCE_URL` | Backend | PostgreSQL connection URL |
| `SPRING_DATASOURCE_USERNAME` | Backend | DB username |
| `SPRING_DATASOURCE_PASSWORD` | Backend | DB password |
| `MAIL_HOST` | Backend | SMTP host (Mailhog in dev, real SMTP in prod) |
| `MAIL_PORT` | Backend | SMTP port |
| `MAIL_USERNAME` | Backend | SMTP username (also IMAP login) |
| `MAIL_PASSWORD` | Backend | SMTP/IMAP password |
| `IMAP_HOST` | Backend | IMAP host for reading the shared inbox |
| `IMAP_PORT` | Backend | IMAP port (default: 993) |
| `IMAP_INBOX_FOLDER` | Backend | IMAP folder to read (default: `INBOX`) |
| `OPENAI_API_KEY` | Backend | OpenAI API key |
| `OPENAI_MODEL` | Backend | OpenAI model (default: `gpt-4o`) |
| `APP_RECIPIENTS` | Backend | Comma-separated list of recipient emails |
| `VITE_API_BASE_URL` | Frontend | Backend API base URL (used by Vite at build time) |
> ⚠️ Never hardcode credentials. Always use environment variables or a `.env` file (gitignored).
---
## Key Domain Concepts
- **VirtualEntity:** A fictional employee of "Condado Abaixo da Média SA". Has a name, a real
email address (used as sender), a job title, a personality description, an email schedule
(cron expression), and an email context window (how many days back to read emails for context).
- **EmailContext:** A snapshot of recent emails read from the shared IMAP inbox, filtered by the
entity's configured context window (e.g., last 3 days). Used to give the AI conversational
context.
- **Prompt:** The full text sent to the OpenAI API. Built by `PromptBuilderService` from the
entity's profile + the `EmailContext`. Always instructs the AI to write in an extremely formal
corporate tone with completely casual/nonsensical content.
- **DispatchLog:** A record of each AI email generation and send attempt for a given entity.
Stores the generated prompt, the AI response, send status, and timestamp.
---
## The Prompt Template (Core Logic)
Every prompt sent to the AI must follow this structure:
```
You are [entity.name], [entity.jobTitle] at "Condado Abaixo da Média SA".
Your personality: [entity.personality]
IMPORTANT TONE RULE: You must write in an extremely formal, bureaucratic, corporate tone —
as if writing an official memo. However, the actual content of the email must be completely
casual, trivial, or nonsensical — as if talking to close friends about mundane things.
The contrast between the formal tone and the casual content is intentional and essential.
Here are the most recent emails from the company inbox (last [entity.contextWindowDays] days)
for context:
[list of recent emails: sender, subject, body]
Write a new email to be sent to the company group, continuing the conversation naturally.
Reply or react to the recent emails if relevant. Sign off as [entity.name], [entity.jobTitle].
Format your response exactly as:
SUBJECT: <subject line here>
BODY:
<full email body here>
```
## Task Preview Generation Rules
- The frontend must **never** call LLM providers (OpenAI/Ollama/Llama) directly.
- The frontend requests backend endpoints only.
- The backend is responsible for:
- building the final prompt,
- calling the configured LLM endpoint,
- returning the generated message to the frontend.
- Generated preview message history must be persisted in the backend database (not browser storage),
so history survives reloads and restarts.
---
## Git Workflow & CI/CD
- Branch naming: `feature/<short-description>`, `fix/<short-description>`, `chore/<short-description>`
- Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `chore:`, `docs:`, `test:`
- Scope your commits: `feat(backend):`, `feat(frontend):`, `chore(docker):`
- **TDD commit order per step:** first `test(<scope>): add failing tests for <step>`, then
`feat(<scope>): implement <step> — all tests passing`.
- PRs require all CI checks to pass before merging.
- Never commit directly to `main`.
### Commit Rules (enforced by AI)
These rules apply to every commit made during AI-assisted implementation:
| Rule | Detail |
|------|--------|
| **Two commits per TDD step** | 1st commit = failing tests (Red), 2nd commit = passing implementation (Green) |
| **Commit after each step** | Never accumulate multiple steps in one commit |
| **Red commit subject** | `test(<scope>): add failing tests for step <N> — <short description>` |
| **Green commit subject** | `feat(<scope>): implement step <N> — <short description>` |
| **Scope values** | `backend`, `frontend`, `docker`, `ci`, `config` |
| **Body** | Optional but encouraged: list what was added/changed |
| **No `--no-verify`** | Never bypass git hooks |
| **No force push** | Never use `--force` on shared branches |
| **Atomic commits** | Each commit must leave the build green (except deliberate Red-phase test commits) |
| **`chore` for housekeeping** | Config changes, dependency tweaks, file renames → `chore(<scope>):` |
| **`fix` for bug fixes** | `fix(<scope>): <what was broken and how it was fixed>` |
| **`docs` for documentation** | Changes to `CLAUDE.md`, `INSTRUCTIONS.md`, `README.md``docs:` |
### Meaningful Commit Quality Bar
Every commit must be understandable without opening the full diff.
- Subject line must explain **intent + scope + outcome**.
- Avoid vague subjects like `update frontend`, `fix stuff`, `changes`, `wip`.
- Keep commits focused on one concern (tests, feature, refactor, docs), not mixed changes.
- Prefer small atomic commits that can be reverted safely.
- Include a commit body when context is not obvious (what changed, why, risks).
Good examples:
- `test(frontend): add failing tests for step 2 - entity task detail flow`
- `feat(frontend): implement step 2 - per-entity scheduled task creation`
- `docs(config): clarify english-first language policy and commit quality rules`
### GitHub Actions Workflows
| Workflow file | Trigger | What it does |
|----------------------------|----------------------------|-----------------------------------------------------------|
| `.github/workflows/ci.yml` | Push / PR to any branch | Backend tests (`./gradlew test`) + Frontend tests (`npm run test`) |
| `.github/workflows/publish.yml` | Push to `main` | Builds `Dockerfile.allinone`, tags as `latest` + git SHA, pushes to Docker Hub |
**Required GitHub Secrets:**
| Secret | Description |
|-----------------------|--------------------------------------------|
| `DOCKERHUB_USERNAME` | Docker Hub account username |
| `DOCKERHUB_TOKEN` | Docker Hub access token (not password) |
**Image tags pushed on every `main` merge:**
- `<dockerhub-user>/condado-newsletter:latest`
- `<dockerhub-user>/condado-newsletter:<git-sha>` (for pinning)
---
## Step 1 Decisions & Versions
| Decision | Detail |
|---|---|
| Gradle wrapper | **8.14.1** (upgraded from 8.7 — Gradle < 8.14 cannot parse Java version `26`) |
| Spring Boot | **3.4.5** (latest stable at time of scaffold) |
| Kotlin | **2.1.21** (latest stable, bundled with Gradle 8.14.1) |
| Java toolchain | **21** configured in `build.gradle.kts` via `kotlin { jvmToolchain(21) }` — bytecode targets Java 21 regardless of host JDK |
| Frontend test script | `vitest run --passWithNoTests` — prevents CI failure before Step 12 adds real tests |