# 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 | Local Docker images on the deployment host (`condado-newsletter-backend`, `condado-newsletter-frontend`) | | CI/CD | Gitea Actions — test PRs to `develop`, deploy approved PRs targeting `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 -d` | Production with prebuilt backend/frontend images | | **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 The all-in-one image is built locally or in external pipelines as needed (no default registry publish workflow in-repo). **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 \ /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/ │ └── (legacy, unused after Gitea migration) ├── .gitea/ │ └── workflows/ │ └── ci.yml ← run tests on pull requests targeting `develop` │ ├── 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 -d # 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` with explicit HTTP status codes. - Services must be annotated with `@Service` and **never** depend on controllers. - Repositories must extend `JpaRepository`. - 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**. --- ## Data Ownership Policy (Critical) - **All business data must be persisted server-side** (PostgreSQL via backend APIs). - The frontend must treat the backend as the single source of truth for entities, tasks, generated preview messages/history, logs, and any other domain data. - The frontend must **not** persist business/domain data in browser storage (`localStorage`, `sessionStorage`, `IndexedDB`) or call LLM providers directly. - The only browser-stored auth state is the backend-issued session token cookie (`httpOnly` JWT). - If a required endpoint does not exist yet, implement it in the backend first; do not add frontend-side persistence workarounds. --- ## 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 for the Vite dev server proxy | > ⚠️ 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: BODY: ``` ## 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 - Git hosting: Gitea instance at `http://gitea.lab`. - Canonical remote: `origin = http://gitea.lab/sancho41/condado-newsletter.git`. - Branch model: **Git Flow** (`main` + `develop` as permanent branches). - Branch naming: `feature/`, `fix/`, `hotfix/`, `release/`, `chore/` - 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(): add failing tests for `, then `feat(): implement — all tests passing`. - Pull requests must target `develop` for regular work. - CI runs on pull requests to `develop` and must pass before merge. - Never commit directly to `main` or `develop`. ### 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(): add failing tests for step ` | | **Green commit subject** | `feat(): implement step ` | | **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():` | | **`fix` for bug fixes** | `fix(): ` | | **`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` ### Gitea Actions Workflows | Workflow file | Trigger | What it does | |----------------------------|----------------------------|-----------------------------------------------------------| | `.gitea/workflows/ci.yml` | PR to `develop` | Backend tests (`./gradlew test`) + Frontend tests (`npm run test`) | | `.gitea/workflows/build.yml` | Approved PR review on `main` | Build `condado-newsletter-backend` and `condado-newsletter-frontend` locally on the runner host | Build policy: the runner shares the target Docker host, so the build workflow produces local Docker images directly on that host. `docker-compose.prod.yml` is image-based and can be started separately without build directives. --- ## 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 |