Files
condado-newsletter/CLAUDE.md
Gabriel Sancho ca2e645f02 feat: initialize frontend with React, Vite, and Tailwind CSS
- Added package.json for project dependencies and scripts.
- Configured PostCSS with Tailwind CSS.
- Created main application structure with App component and routing.
- Implemented API client for handling requests with Axios.
- Developed authentication API for login, logout, and user verification.
- Created entities API for managing virtual entities.
- Implemented logs API for fetching dispatch logs.
- Added navigation bar component for app navigation.
- Created protected route component for route guarding.
- Set up global CSS with Tailwind directives.
- Configured main entry point for React application.
- Developed basic Dashboard and Login pages.
- Set up router for application navigation.
- Added Jest testing setup for testing library.
- Configured Tailwind CSS with content paths.
- Set TypeScript configuration for frontend.
- Created Vite configuration for development and production builds.
- Added Nginx configuration for serving the application and proxying API requests.
2026-03-26 15:04:12 -03:00

493 lines
24 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.
---
## 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
### 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
### Backend
- Every service class must have a corresponding unit test class.
- 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.
### Frontend
- Every page component must have at least a smoke test (renders without crashing).
- API layer functions must be tested with mocked Axios responses.
- Use **Vitest** as the test runner and **React Testing Library** for component tests.
---
## 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>
```
---
## 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):`
- PRs require all CI checks to pass before merging.
- Never commit directly to `main`.
### 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 |