From 46f78467bb95e9b0108dea1092f6dad69255730b Mon Sep 17 00:00:00 2001 From: Gabriel Sancho Date: Fri, 27 Mar 2026 16:18:58 -0300 Subject: [PATCH] fix: add missing colon in Active Entities label on DashboardPage (#1) Reviewed-on: http://gitea.lab/sancho41/condado-newsletter/pulls/1 Co-authored-by: Gabriel Sancho Co-committed-by: Gabriel Sancho --- .env.example | 2 +- .gitea/workflows/build.yml | 38 ++++++++++++++++ .gitea/workflows/ci.yml | 4 ++ .github/agents/infra.agent.md | 12 ++--- CLAUDE.md | 19 ++++---- Dockerfile.allinone | 1 + docker-compose.prod.yml | 66 ++++++++-------------------- docker/entrypoint.sh | 14 +++--- frontend/src/pages/DashboardPage.tsx | 2 +- 9 files changed, 89 insertions(+), 69 deletions(-) create mode 100644 .gitea/workflows/build.yml diff --git a/.env.example b/.env.example index b4b88f4..a42c0ff 100644 --- a/.env.example +++ b/.env.example @@ -33,5 +33,5 @@ LLAMA_MODEL=gemma3:4b # ── Application ─────────────────────────────────────────────────────────────── APP_RECIPIENTS=friend1@example.com,friend2@example.com -# ── Frontend (Vite build-time) ──────────────────────────────────────────────── +# ── Frontend (Vite dev proxy) ───────────────────────────────────────────────── VITE_API_BASE_URL=http://localhost diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..4fd0dd2 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,38 @@ +name: Build And Publish Production Image + +on: + pull_request_review: + types: [submitted] + +jobs: + build: + name: Build And Publish Production Image + if: github.event.review.state == 'approved' && github.event.pull_request.base.ref == 'main' + runs-on: ubuntu-latest + env: + REGISTRY: gitea.lab + IMAGE_NAME: sancho41/condado-newsletter + steps: + - uses: actions/checkout@v4 + with: + github-server-url: http://gitea.lab + ref: ${{ github.event.pull_request.head.sha }} + + - name: Verify Docker CLI + run: docker version + + - name: Build all-in-one image + run: docker build -t condado-newsletter:latest -f Dockerfile.allinone . + + - name: Log in to Gitea container registry + run: echo "${{ secrets.GITEA_REGISTRY_PASSWORD }}" | docker login ${REGISTRY} -u "${{ secrets.GITEA_REGISTRY_USERNAME }}" --password-stdin + + - name: Tag registry images + run: | + docker tag condado-newsletter:latest ${REGISTRY}/${IMAGE_NAME}:latest + docker tag condado-newsletter:latest ${REGISTRY}/${IMAGE_NAME}:${{ github.sha }} + + - name: Push registry images + run: | + docker push ${REGISTRY}/${IMAGE_NAME}:latest + docker push ${REGISTRY}/${IMAGE_NAME}:${{ github.sha }} \ No newline at end of file diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 3b1d320..276dcd4 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -13,6 +13,8 @@ jobs: working-directory: backend steps: - uses: actions/checkout@v4 + with: + github-server-url: http://gitea.lab - name: Set up JDK 21 uses: actions/setup-java@v4 @@ -42,6 +44,8 @@ jobs: working-directory: frontend steps: - uses: actions/checkout@v4 + with: + github-server-url: http://gitea.lab - name: Set up Node 20 uses: actions/setup-node@v4 diff --git a/.github/agents/infra.agent.md b/.github/agents/infra.agent.md index ebb3f4c..02ca393 100644 --- a/.github/agents/infra.agent.md +++ b/.github/agents/infra.agent.md @@ -1,6 +1,6 @@ --- name: infra -description: "Use when working on Docker configuration, Docker Compose files, Dockerfiles, Nginx config, Supervisor config, Gitea Actions workflows, CI/CD pipelines, environment variables, or overall project architecture in the condado-news-letter project. Trigger phrases: docker, dockerfile, compose, nginx, ci/cd, gitea actions, build fails, infra, architecture, environment variables, container, supervisor, allinone image." +description: "Use when working on Docker configuration, Docker Compose files, Dockerfiles, Nginx config, Supervisor config, Gitea Actions workflows, CI/CD pipelines, deploy flows, environment variables, or overall project architecture in the condado-news-letter project. Trigger phrases: docker, dockerfile, compose, nginx, ci/cd, gitea actions, deploy, build fails, infra, architecture, environment variables, container, supervisor, allinone image." tools: [read, edit, search, execute, todo] argument-hint: "Describe the infrastructure change or Docker/CI task to implement." --- @@ -15,13 +15,14 @@ You are a senior DevOps / infrastructure engineer and software architect for the | `backend/Dockerfile` | Backend-only multi-stage build image | | `frontend/Dockerfile` | Frontend build + Nginx image | | `docker-compose.yml` | Dev stack (postgres + backend + nginx + mailhog) | -| `docker-compose.prod.yml` | Prod stack (postgres + backend + nginx, no mailhog) | +| `docker-compose.prod.yml` | Prod stack (single all-in-one image) | | `nginx/nginx.conf` | Nginx config for multi-container compose flavours | | `nginx/nginx.allinone.conf` | Nginx config for the all-in-one image (localhost backend) | | `frontend/nginx.docker.conf` | Nginx config embedded in frontend image | | `docker/supervisord.conf` | Supervisor config (manages postgres + java + nginx inside allinone) | | `docker/entrypoint.sh` | Allinone container entrypoint (DB init, env wiring, supervisord start) | | `.gitea/workflows/ci.yml` | CI: backend tests + frontend tests on pull requests to `develop` | +| `.gitea/workflows/build.yml` | Build: create and publish the all-in-one image on approved PRs to `main` | | `.env.example` | Template for all environment variables | ## System Topology @@ -53,7 +54,7 @@ Docker volume → /var/lib/postgresql/data | Flavour | Command | Notes | |---|---|---| | Dev | `docker compose up --build` | Includes Mailhog on :1025/:8025 | -| Prod (compose) | `docker compose -f docker-compose.prod.yml up --build` | External DB/SMTP | +| Prod (compose) | `docker compose -f docker-compose.prod.yml up -d` | Prebuilt all-in-one image with internal PostgreSQL | | All-in-one | `docker run -p 80:80 -e APP_PASSWORD=... ` | Everything in one container | ## Key Environment Variables @@ -73,15 +74,16 @@ All injected at runtime — never hardcoded in images. | `IMAP_HOST` / `IMAP_PORT` / `IMAP_INBOX_FOLDER` | Backend | IMAP server | | `OPENAI_API_KEY` / `OPENAI_MODEL` | Backend | OpenAI credentials | | `APP_RECIPIENTS` | Backend | Comma-separated recipient emails | -| `VITE_API_BASE_URL` | Frontend (build-time ARG) | Backend API base URL | +| `VITE_API_BASE_URL` | Frontend dev server | Backend API base URL for Vite proxy | ## CI/CD Pipeline | Workflow | Trigger | What it does | |---|---|---| | `ci.yml` | Pull request to `develop` | Backend `./gradlew test` + Frontend `npm run test` | +| `build.yml` | Approved PR review to `main` | Builds `condado-newsletter` on the target Docker host, then pushes `latest` and `${github.sha}` tags to Gitea container registry | -Legacy publish/version workflows were removed from in-repo automation. +The runner shares the target Docker host, so this workflow builds the image locally, tags it for `gitea.lab/sancho41/condado-newsletter`, and pushes it to Gitea container registry. `docker-compose.prod.yml` must reference that published image and not local build directives. ## Implementation Rules diff --git a/CLAUDE.md b/CLAUDE.md index 3096628..0773977 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,8 +83,8 @@ The cycle for every step is: | 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 | Not configured (legacy Docker Hub publish workflow removed) | -| CI/CD | Gitea Actions — run backend/frontend tests on pull requests to `develop` | +| Image registry | Gitea container registry (`gitea.lab/sancho41/condado-newsletter`) | +| CI/CD | Gitea Actions — test PRs to `develop`, build and publish the production image on approved PRs targeting `main` | ## Deployment Flavours @@ -93,7 +93,7 @@ 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 | +| **Prod (compose)** | `docker compose -f docker-compose.prod.yml up -d` | Production with the prebuilt all-in-one image | | **All-in-one** | `docker run ...` | Simplest deploy — everything in one container | ### All-in-one Image @@ -104,7 +104,7 @@ The all-in-one image (`Dockerfile.allinone`) bundles **everything** into a singl - **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). +The all-in-one image is built on the runner host and then published to the Gitea container registry. **Minimal `docker run` command:** ```bash @@ -121,7 +121,7 @@ docker run -d \ -e IMAP_PORT=993 \ -e APP_RECIPIENTS=friend1@example.com,friend2@example.com \ -v condado-data:/var/lib/postgresql/data \ - /condado-newsletter:latest + gitea.lab/sancho41/condado-newsletter:latest ``` The app is then available at `http://localhost`. @@ -213,7 +213,7 @@ condado-news-letter/ ← repo root ├── .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) +├── docker-compose.prod.yml ← prod stack (single all-in-one image) ├── Dockerfile.allinone ← all-in-one image (Nginx + Backend + PostgreSQL + Supervisor) │ ├── .github/ @@ -312,7 +312,7 @@ npm run test docker compose up --build # Prod -docker compose -f docker-compose.prod.yml up --build +docker compose -f docker-compose.prod.yml up -d # Stop docker compose down @@ -456,7 +456,7 @@ Never hardcode any of these values. | `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) | +| `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). @@ -575,8 +575,9 @@ Good examples: | 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`, then publish `latest` and `${github.sha}` tags to Gitea container registry | -Current policy: old publish/version automation workflows were removed during the Gitea migration. +Build policy: the runner shares the target Docker host, so the build workflow produces the image locally, tags it for `gitea.lab/sancho41/condado-newsletter`, and pushes it to Gitea container registry. `docker-compose.prod.yml` references that published image. --- diff --git a/Dockerfile.allinone b/Dockerfile.allinone index 5f411d3..b842548 100644 --- a/Dockerfile.allinone +++ b/Dockerfile.allinone @@ -15,6 +15,7 @@ FROM gradle:8-jdk21-alpine AS backend-build WORKDIR /app/backend COPY backend/build.gradle.kts backend/settings.gradle.kts ./ +COPY backend/gradle.properties ./ COPY backend/gradle ./gradle RUN gradle dependencies --no-daemon --quiet || true diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index da14aa1..1391c39 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,35 +1,10 @@ services: - - # ── PostgreSQL ─────────────────────────────────────────────────────────────── - postgres: - image: postgres:16-alpine - restart: always - environment: - POSTGRES_DB: condado - POSTGRES_USER: ${SPRING_DATASOURCE_USERNAME} - POSTGRES_PASSWORD: ${SPRING_DATASOURCE_PASSWORD} - volumes: - - postgres-data:/var/lib/postgresql/data - networks: - - condado-net - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${SPRING_DATASOURCE_USERNAME} -d condado"] - interval: 10s - timeout: 5s - retries: 5 - - # ── Backend (Spring Boot) ──────────────────────────────────────────────────── - backend: - build: - context: ./backend - dockerfile: Dockerfile - restart: always - depends_on: - postgres: - condition: service_healthy + condado-newsletter: + image: gitea.lab/sancho41/condado-newsletter:latest + container_name: condado-newsletter + restart: unless-stopped environment: SPRING_PROFILES_ACTIVE: prod - SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL} SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME} SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD} APP_PASSWORD: ${APP_PASSWORD} @@ -50,27 +25,22 @@ services: extra_hosts: - "celtinha.desktop:host-gateway" - "host.docker.internal:host-gateway" - networks: - - condado-net - - # ── Frontend + Nginx ───────────────────────────────────────────────────────── - nginx: - build: - context: ./frontend - dockerfile: Dockerfile - args: - VITE_API_BASE_URL: ${VITE_API_BASE_URL} - restart: always - ports: - - "80:80" - depends_on: - - backend - networks: - - condado-net + volumes: + - postgres-data:/var/lib/postgresql/data + labels: + - "traefik.enable=true" + - "traefik.http.routers.condado.rule=Host(`condado-newsletter.lab`)" + - "traefik.http.services.condado.loadbalancer.server.port=80" + - "homepage.group=Hyperlink" + - "homepage.name=Condado Newsletter" + - "homepage.description=Automated newsletter generator using AI" + - "homepage.logo=https://raw.githubusercontent.com/celtinha/condado-newsletter/main/docs/logo.png" + - "homepage.url=http://condado-newsletter.lab" volumes: postgres-data: networks: - condado-net: - driver: bridge + default: + name: traefik + external: true diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 0493261..6a6f2c1 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,6 +1,10 @@ #!/bin/bash set -e +APP_DB_NAME=${APP_DB_NAME:-condado} +APP_DB_USER=${SPRING_DATASOURCE_USERNAME:-condado} +APP_DB_PASSWORD=${SPRING_DATASOURCE_PASSWORD:-condado} + # ── Initialise PostgreSQL data directory on first run ───────────────────────── if [ ! -f /var/lib/postgresql/data/PG_VERSION ]; then echo "Initialising PostgreSQL data directory..." @@ -9,8 +13,8 @@ if [ ! -f /var/lib/postgresql/data/PG_VERSION ]; then # Start postgres temporarily to create the app database and user su -c "/usr/lib/postgresql/16/bin/pg_ctl -D /var/lib/postgresql/data -w start" postgres - su -c "psql -c \"CREATE USER condado WITH PASSWORD 'condado';\"" postgres - su -c "psql -c \"CREATE DATABASE condado OWNER condado;\"" postgres + su -c "psql -v ON_ERROR_STOP=1 -c \"CREATE USER ${APP_DB_USER} WITH PASSWORD '${APP_DB_PASSWORD}';\"" postgres + su -c "psql -v ON_ERROR_STOP=1 -c \"CREATE DATABASE ${APP_DB_NAME} OWNER ${APP_DB_USER};\"" postgres su -c "/usr/lib/postgresql/16/bin/pg_ctl -D /var/lib/postgresql/data -w stop" postgres echo "PostgreSQL initialised." @@ -20,9 +24,9 @@ fi mkdir -p /var/log/supervisor # ── Defaults for all-in-one local PostgreSQL ───────────────────────────────── -export SPRING_DATASOURCE_URL=${SPRING_DATASOURCE_URL:-jdbc:postgresql://localhost:5432/condado} -export SPRING_DATASOURCE_USERNAME=${SPRING_DATASOURCE_USERNAME:-condado} -export SPRING_DATASOURCE_PASSWORD=${SPRING_DATASOURCE_PASSWORD:-condado} +export SPRING_DATASOURCE_URL=${SPRING_DATASOURCE_URL:-jdbc:postgresql://localhost:5432/${APP_DB_NAME}} +export SPRING_DATASOURCE_USERNAME=${SPRING_DATASOURCE_USERNAME:-${APP_DB_USER}} +export SPRING_DATASOURCE_PASSWORD=${SPRING_DATASOURCE_PASSWORD:-${APP_DB_PASSWORD}} # ── Start all services via supervisord ─────────────────────────────────────── exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 6af4cca..288ef21 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -27,7 +27,7 @@ export default function DashboardPage() {
-

Active Entities

+

Active Entities:

{activeCount} active {activeCount === 1 ? 'entity' : 'entities'}