Refactor project instructions and structure for monorepo setup

- Updated Step 1 to scaffold a monorepo structure for both backend and frontend.
- Renamed dependencies and adjusted project structure in INSTRUCTIONS.md.
- Added frontend dependencies and outlined the React application structure.
- Revised authentication method from API key to JWT for enhanced security.
- Created detailed instructions for frontend development, including page structure and API integration.
- Added steps for Docker configuration, including an all-in-one Docker image for deployment.
- Implemented CI/CD workflows for automated testing and Docker Hub publishing.
This commit is contained in:
2026-03-26 14:31:25 -03:00
parent d834ca85b0
commit fa6731de98
2 changed files with 702 additions and 505 deletions

413
CLAUDE.md
View File

@@ -1,29 +1,31 @@
# Condado Abaixo da Média SA — Email Bot
A backend service built with **Kotlin** and **Spring Boot**. This file gives the AI persistent
instructions and context about the project so every session starts with the right knowledge.
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
- **Language:** Kotlin (JVM)
- **Framework:** Spring Boot 3.x
- **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 a virtual 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
(filtered by a configurable time window), builds a prompt from the entity's profile + the email
history, sends that prompt to an AI (OpenAI API), and dispatches the AI-generated email via SMTP.
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:** REST API backend + scheduled AI-driven email dispatch
- **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) |
@@ -32,64 +34,270 @@ instructions and context about the project so every session starts with the righ
| 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 HTTP client |
| Scheduler | Spring `@Scheduled` tasks |
| 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`.
---
## Project Structure
## 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
```
src/
├── main/
│ ├── kotlin/com/condado/newsletter/
│ │ ├── CondadoApplication.kt # App entry point
│ │ ├── config/ # Spring configuration classes
│ │ ├── controller/ # REST controllers
│ │ ├── service/ # Business logic
│ │ │ ├── EntityService.kt # CRUD for virtual entities
│ │ ├── EmailReaderService.kt # Reads emails via IMAP
│ │ │ ├── PromptBuilderService.kt # Builds AI prompt from entity + emails
│ │ ├── AiService.kt # Calls OpenAI API
│ └── EmailSenderService.kt # Sends email via SMTP
── repository/ # Spring Data JPA repositories
│ ├── model/ # JPA entities
│ │ ├── dto/ # Data Transfer Objects
│ └── scheduler/ # Scheduled tasks (trigger per entity)
── resources/
├── application.yml # Main config
└── application-dev.yml # Dev profile config
└── test/
└── kotlin/com/condado/newsletter/ # Tests mirror main 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 the application (dev profile)
# Run (dev profile)
./gradlew bootRun --args='--spring.profiles.active=dev'
# Run all tests
# Run tests
./gradlew test
# Run a specific test class
# Run a specific test
./gradlew test --tests "com.condado.newsletter.service.PromptBuilderServiceTest"
```
# OpenAPI docs available at runtime
# http://localhost:8080/swagger-ui.html
### 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`).
@@ -101,52 +309,96 @@ src/
- 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` — no other
class should build or modify prompt strings.
- 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
| 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` |
### 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 the pattern: `should_[expectedBehavior]_when_[condition]`.
- 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
| Variable | Description |
|---------------------------|------------------------------------------------------|
| `SPRING_DATASOURCE_URL` | PostgreSQL connection URL |
| `SPRING_DATASOURCE_USERNAME` | DB username |
| `SPRING_DATASOURCE_PASSWORD` | DB password |
| `MAIL_HOST` | SMTP host (for sending emails) |
| `MAIL_PORT` | SMTP port |
| `MAIL_USERNAME` | SMTP username (also used as IMAP login) |
| `MAIL_PASSWORD` | SMTP/IMAP password |
| `IMAP_HOST` | IMAP host (for reading the shared inbox) |
| `IMAP_PORT` | IMAP port (default: 993) |
| `IMAP_INBOX_FOLDER` | IMAP folder to read (default: `INBOX`) |
| `OPENAI_API_KEY` | OpenAI API key for AI generation |
| `OPENAI_MODEL` | OpenAI model to use (default: `gpt-4o`) |
| `API_KEY` | API key to protect the REST endpoints |
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).
@@ -159,7 +411,8 @@ src/
(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.
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
@@ -191,13 +444,37 @@ for context:
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
## 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:`
- PRs require at least one passing CI check before merging.
- 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)