593 lines
30 KiB
Markdown
593 lines
30 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 | 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 \
|
|
<registry-or-local-image>/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<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**.
|
|
|
|
---
|
|
|
|
## 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: <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
|
|
|
|
- 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/<short-description>`, `fix/<short-description>`, `hotfix/<short-description>`, `release/<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`.
|
|
- 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(<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`
|
|
|
|
### 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 |
|