From 46391948b374b3123265cd9cb97eec4c7e781b0e Mon Sep 17 00:00:00 2001 From: Gabriel Sancho Date: Fri, 27 Mar 2026 15:28:12 -0300 Subject: [PATCH 1/7] fix: add missing colon in Active Entities label on DashboardPage --- frontend/src/pages/DashboardPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'}

From 06112330b63e857383d6434f61d61480e58b8c88 Mon Sep 17 00:00:00 2001 From: Gabriel Sancho Date: Fri, 27 Mar 2026 15:35:32 -0300 Subject: [PATCH 2/7] fix(ci): add missing 'with' block for checkout step in backend and frontend jobs --- .gitea/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) 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 From 3f0bb4be736e9ac92b1f7072cf8bdaf534f3fcf9 Mon Sep 17 00:00:00 2001 From: Gabriel Sancho Date: Fri, 27 Mar 2026 16:01:34 -0300 Subject: [PATCH 3/7] feat: update Docker configuration and CI/CD workflows for local image builds --- .env.example | 2 +- .gitea/workflows/build.yml | 25 +++++++++++++++++++++++++ .github/agents/infra.agent.md | 10 ++++++---- CLAUDE.md | 13 +++++++------ docker-compose.prod.yml | 10 ++-------- 5 files changed, 41 insertions(+), 19 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..ab4e5e5 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,25 @@ +name: Build Production Images + +on: + pull_request_review: + types: [submitted] + +jobs: + build: + name: Build Production Images + if: github.event.review.state == 'approved' && github.event.pull_request.base.ref == 'main' + runs-on: ubuntu-latest + 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 backend image + run: docker build -t condado-newsletter-backend:latest -f backend/Dockerfile ./backend + + - name: Build frontend image + run: docker build -t condado-newsletter-frontend:latest -f frontend/Dockerfile ./frontend \ No newline at end of file diff --git a/.github/agents/infra.agent.md b/.github/agents/infra.agent.md index ebb3f4c..72c05aa 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." --- @@ -22,6 +22,7 @@ You are a senior DevOps / infrastructure engineer and software architect for the | `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 local backend/frontend images 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` | External DB/SMTP using prebuilt local images | | 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-backend` and `condado-newsletter-frontend` on the target Docker host | -Legacy publish/version workflows were removed from in-repo automation. +The runner shares the target Docker host, so this workflow produces local images directly on that host. `docker-compose.prod.yml` must reference images and not local build directives. ## Implementation Rules diff --git a/CLAUDE.md b/CLAUDE.md index 3096628..fd8a318 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 | 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 @@ -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 prebuilt backend/frontend images | | **All-in-one** | `docker run ...` | Simplest deploy — everything in one container | ### All-in-one Image @@ -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-backend` and `condado-newsletter-frontend` locally on the runner host | -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 local Docker images directly on that host. `docker-compose.prod.yml` is image-based and can be started separately without build directives. --- diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index da14aa1..cc16bfa 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -20,9 +20,7 @@ services: # ── Backend (Spring Boot) ──────────────────────────────────────────────────── backend: - build: - context: ./backend - dockerfile: Dockerfile + image: condado-newsletter-backend:latest restart: always depends_on: postgres: @@ -55,11 +53,7 @@ services: # ── Frontend + Nginx ───────────────────────────────────────────────────────── nginx: - build: - context: ./frontend - dockerfile: Dockerfile - args: - VITE_API_BASE_URL: ${VITE_API_BASE_URL} + image: condado-newsletter-frontend:latest restart: always ports: - "80:80" From 6305a8e95e27bb26d69b4b144892938317c9cf52 Mon Sep 17 00:00:00 2001 From: Gabriel Sancho Date: Fri, 27 Mar 2026 16:10:14 -0300 Subject: [PATCH 4/7] refactor: update build process to create a single all-in-one Docker image and adjust related configurations --- .gitea/workflows/build.yml | 11 +++---- .github/agents/infra.agent.md | 10 +++--- CLAUDE.md | 10 +++--- Dockerfile.allinone | 1 + docker-compose.prod.yml | 60 +++++++++++------------------------ docker/entrypoint.sh | 14 +++++--- 6 files changed, 42 insertions(+), 64 deletions(-) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index ab4e5e5..e8d47b9 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build Production Images +name: Build Production Image on: pull_request_review: @@ -6,7 +6,7 @@ on: jobs: build: - name: Build Production Images + name: Build Production Image if: github.event.review.state == 'approved' && github.event.pull_request.base.ref == 'main' runs-on: ubuntu-latest steps: @@ -18,8 +18,5 @@ jobs: - name: Verify Docker CLI run: docker version - - name: Build backend image - run: docker build -t condado-newsletter-backend:latest -f backend/Dockerfile ./backend - - - name: Build frontend image - run: docker build -t condado-newsletter-frontend:latest -f frontend/Dockerfile ./frontend \ No newline at end of file + - name: Build all-in-one image + run: docker build -t condado-newsletter:latest -f Dockerfile.allinone . \ No newline at end of file diff --git a/.github/agents/infra.agent.md b/.github/agents/infra.agent.md index 72c05aa..ea5883e 100644 --- a/.github/agents/infra.agent.md +++ b/.github/agents/infra.agent.md @@ -15,14 +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 local backend/frontend images on approved PRs to `main` | +| `.gitea/workflows/build.yml` | Build: create the local all-in-one image on approved PRs to `main` | | `.env.example` | Template for all environment variables | ## System Topology @@ -54,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 -d` | External DB/SMTP using prebuilt local images | +| 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 @@ -81,9 +81,9 @@ All injected at runtime — never hardcoded in images. | 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-backend` and `condado-newsletter-frontend` on the target Docker host | +| `build.yml` | Approved PR review to `main` | Builds `condado-newsletter` on the target Docker host | -The runner shares the target Docker host, so this workflow produces local images directly on that host. `docker-compose.prod.yml` must reference images and not local build directives. +The runner shares the target Docker host, so this workflow produces the local `condado-newsletter` image directly on that host. `docker-compose.prod.yml` must reference that image and not local build directives. ## Implementation Rules diff --git a/CLAUDE.md b/CLAUDE.md index fd8a318..cafcdb3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,7 +83,7 @@ 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 | Local Docker images on the deployment host (`condado-newsletter-backend`, `condado-newsletter-frontend`) | +| Image registry | Local Docker image on the deployment host (`condado-newsletter`) | | CI/CD | Gitea Actions — test PRs to `develop`, deploy 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 -d` | Production with prebuilt backend/frontend images | +| **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 @@ -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/ @@ -575,9 +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-backend` and `condado-newsletter-frontend` locally on the runner host | +| `.gitea/workflows/build.yml` | Approved PR review on `main` | Build `condado-newsletter` 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. +Build policy: the runner shares the target Docker host, so the build workflow produces the local `condado-newsletter` image directly on that host. `docker-compose.prod.yml` is image-based and can be started separately without build directives. --- 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 cc16bfa..9a85feb 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,33 +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: - image: condado-newsletter-backend:latest - restart: always - depends_on: - postgres: - condition: service_healthy + condado-newsletter: + image: 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} @@ -48,23 +25,22 @@ services: extra_hosts: - "celtinha.desktop:host-gateway" - "host.docker.internal:host-gateway" - networks: - - condado-net - - # ── Frontend + Nginx ───────────────────────────────────────────────────────── - nginx: - image: condado-newsletter-frontend:latest - 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 From d6de131a9bfe82de22e1a743bba3ecf4c211bbe8 Mon Sep 17 00:00:00 2001 From: Gabriel Sancho Date: Fri, 27 Mar 2026 16:18:25 -0300 Subject: [PATCH 5/7] feat: update build workflow to create and publish all-in-one Docker image on approved PRs --- .gitea/workflows/build.yml | 22 +++++++++++++++++++--- .github/agents/infra.agent.md | 6 +++--- CLAUDE.md | 12 ++++++------ docker-compose.prod.yml | 2 +- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index e8d47b9..4fd0dd2 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build Production Image +name: Build And Publish Production Image on: pull_request_review: @@ -6,9 +6,12 @@ on: jobs: build: - name: Build Production Image + 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: @@ -19,4 +22,17 @@ jobs: run: docker version - name: Build all-in-one image - run: docker build -t condado-newsletter:latest -f Dockerfile.allinone . \ No newline at end of file + 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/.github/agents/infra.agent.md b/.github/agents/infra.agent.md index ea5883e..02ca393 100644 --- a/.github/agents/infra.agent.md +++ b/.github/agents/infra.agent.md @@ -22,7 +22,7 @@ You are a senior DevOps / infrastructure engineer and software architect for the | `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 the local all-in-one image on approved PRs to `main` | +| `.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 @@ -81,9 +81,9 @@ All injected at runtime — never hardcoded in images. | 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 | +| `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 | -The runner shares the target Docker host, so this workflow produces the local `condado-newsletter` image directly on that host. `docker-compose.prod.yml` must reference that image and not local build directives. +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 cafcdb3..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 | Local Docker image on the deployment host (`condado-newsletter`) | -| CI/CD | Gitea Actions — test PRs to `develop`, deploy approved PRs targeting `main` | +| 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 @@ -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`. @@ -575,9 +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` locally on the runner host | +| `.gitea/workflows/build.yml` | Approved PR review on `main` | Build `condado-newsletter`, then publish `latest` and `${github.sha}` tags to Gitea container registry | -Build policy: the runner shares the target Docker host, so the build workflow produces the local `condado-newsletter` image directly on that host. `docker-compose.prod.yml` is image-based and can be started separately without build directives. +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/docker-compose.prod.yml b/docker-compose.prod.yml index 9a85feb..1391c39 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,6 +1,6 @@ services: condado-newsletter: - image: condado-newsletter:latest + image: gitea.lab/sancho41/condado-newsletter:latest container_name: condado-newsletter restart: unless-stopped environment: From 5723c74e39cbf5ff5e5e13d9f51540c006dc8e04 Mon Sep 17 00:00:00 2001 From: Gabriel Sancho Date: Fri, 27 Mar 2026 16:19:26 -0300 Subject: [PATCH 6/7] fix: add missing colon in Active Entities label on DashboardPage (#1) (#2) Reviewed-on: http://gitea.lab/sancho41/condado-newsletter/pulls/1 Co-authored-by: Gabriel Sancho Co-committed-by: Gabriel Sancho Reviewed-on: http://gitea.lab/sancho41/condado-newsletter/pulls/2 --- .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'}

From 81d04b63d18ecf3812d989363249b9634a5c3246 Mon Sep 17 00:00:00 2001 From: Gabriel Sancho Date: Fri, 27 Mar 2026 16:23:13 -0300 Subject: [PATCH 7/7] develop (#4) Reviewed-on: http://gitea.lab/sancho41/condado-newsletter/pulls/4 Co-authored-by: Gabriel Sancho Co-committed-by: Gabriel Sancho