From 448088e4d9d5d577e78b4e6deca388f705cb17ac Mon Sep 17 00:00:00 2001
From: Joaquin Hernandez Martinez
Date: Wed, 15 Apr 2026 13:14:33 +0200
Subject: [PATCH 1/8] v1.0.5 - Added CI/CD
---
.github/CODEOWNERS | 5 +
.github/dependabot.yml | 40 ++++
.github/workflows/auto-merge.yml | 204 ++++++++++++++++
.github/workflows/ci.yml | 127 ++++++++++
.github/workflows/codeql.yml | 36 +++
.github/workflows/notify-production.yml | 81 +++++++
project/project/settings.py | 2 +-
pyproject.toml | 2 +-
readme.md | 299 +++++++++++++++++++++++-
uv.lock | 2 +-
10 files changed, 787 insertions(+), 11 deletions(-)
create mode 100644 .github/CODEOWNERS
create mode 100644 .github/dependabot.yml
create mode 100644 .github/workflows/auto-merge.yml
create mode 100644 .github/workflows/ci.yml
create mode 100644 .github/workflows/codeql.yml
create mode 100644 .github/workflows/notify-production.yml
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..a2b9a1f
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,5 @@
+# Five a Day — Code Owners
+# Docs: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
+
+# Both accounts own everything
+* @starseeker-code-public @starseeker-code
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..671ec87
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,40 @@
+version: 2
+
+updates:
+ # Python dependencies (pip / uv)
+ - package-ecosystem: pip
+ directory: /
+ schedule:
+ interval: weekly
+ day: monday
+ time: "08:00"
+ timezone: "Europe/Madrid"
+ open-pull-requests-limit: 5
+ target-branch: development
+ labels:
+ - dependencies
+ - python
+ groups:
+ # Group all non-breaking minor/patch updates into one PR
+ python-minor-patch:
+ update-types:
+ - minor
+ - patch
+ ignore:
+ # Django major versions require manual upgrade planning
+ - dependency-name: django
+ update-types: [version-update:semver-major]
+
+ # GitHub Actions
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ day: monday
+ time: "08:00"
+ timezone: "Europe/Madrid"
+ open-pull-requests-limit: 5
+ target-branch: development
+ labels:
+ - dependencies
+ - github-actions
diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml
new file mode 100644
index 0000000..e68195b
--- /dev/null
+++ b/.github/workflows/auto-merge.yml
@@ -0,0 +1,204 @@
+name: Auto-merge development → testing
+
+# Runs every hour. Merges development into testing when:
+# 1. development has commits not yet in testing
+# 2. the last commit on development is at least 24h old
+# 3. CI is passing on the latest development commit
+# After a successful merge, creates a PR from testing → main (if one doesn't exist).
+
+on:
+ schedule:
+ - cron: '0 * * * *' # Every hour at :00
+ workflow_dispatch: # Manual trigger from the Actions tab
+
+permissions:
+ contents: write
+ pull-requests: write
+ checks: read
+
+jobs:
+ auto-merge:
+ name: Check and merge
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ # Use a PAT so that the push and PR creation trigger downstream CI.
+ # Falls back to GITHUB_TOKEN if GH_PAT is not configured.
+ token: ${{ secrets.GH_PAT || github.token }}
+
+ - name: Check merge conditions
+ id: conditions
+ run: |
+ git fetch origin
+
+ # 1. Is development ahead of testing?
+ AHEAD=$(git rev-list --count origin/testing..origin/development)
+ if [ "$AHEAD" -eq 0 ]; then
+ echo "development is not ahead of testing — nothing to do."
+ echo "should_merge=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ # 2. Is the last commit on development at least 24 hours old?
+ LAST_COMMIT_EPOCH=$(git log origin/development -1 --format="%ct")
+ NOW_EPOCH=$(date +%s)
+ AGE=$(( NOW_EPOCH - LAST_COMMIT_EPOCH ))
+ echo "Last commit age: ${AGE}s (need ≥ 86400)"
+
+ if [ "$AGE" -lt 86400 ]; then
+ echo "Last commit is less than 24 h old — waiting."
+ echo "should_merge=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ LAST_SHA=$(git rev-parse origin/development)
+ LAST_MSG=$(git log origin/development -1 --format="%s")
+
+ echo "should_merge=true" >> "$GITHUB_OUTPUT"
+ echo "last_sha=$LAST_SHA" >> "$GITHUB_OUTPUT"
+ echo "last_msg=$LAST_MSG" >> "$GITHUB_OUTPUT"
+
+ - name: Check CI status on development
+ id: ci
+ if: steps.conditions.outputs.should_merge == 'true'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const sha = '${{ steps.conditions.outputs.last_sha }}';
+
+ const { data } = await github.rest.checks.listForRef({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ ref: sha,
+ filter: 'latest',
+ per_page: 100
+ });
+
+ // Exclude this workflow itself from the check
+ const relevant = data.check_runs.filter(r => r.name !== 'Check and merge');
+
+ if (relevant.length === 0) {
+ console.log('No CI runs found on this commit — proceeding.');
+ core.setOutput('passed', 'true');
+ return;
+ }
+
+ const inProgress = relevant.filter(r => r.status !== 'completed');
+ if (inProgress.length > 0) {
+ console.log('CI still in progress:', inProgress.map(r => r.name).join(', '));
+ core.setOutput('passed', 'false');
+ return;
+ }
+
+ const failed = relevant.filter(
+ r => !['success', 'skipped', 'neutral'].includes(r.conclusion)
+ );
+ if (failed.length > 0) {
+ console.log('CI failed:', failed.map(r => `${r.name}: ${r.conclusion}`).join(', '));
+ core.setOutput('passed', 'false');
+ } else {
+ console.log('CI passed on', sha);
+ core.setOutput('passed', 'true');
+ }
+
+ - name: Merge development into testing
+ id: merge
+ if: >
+ steps.conditions.outputs.should_merge == 'true' &&
+ steps.ci.outputs.passed == 'true'
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ DATE=$(date +%Y-%m-%d)
+ LAST_MSG="${{ steps.conditions.outputs.last_msg }}"
+ MERGE_MSG="$DATE - $LAST_MSG"
+
+ git checkout -B testing origin/testing
+ git merge origin/development --no-ff -m "$MERGE_MSG"
+ git push origin testing
+
+ echo "merge_msg=$MERGE_MSG" >> "$GITHUB_OUTPUT"
+ echo "Merged with message: $MERGE_MSG"
+
+ - name: Create PR testing → main
+ id: pr
+ if: steps.merge.conclusion == 'success'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ // Skip if a PR already exists
+ const existing = await github.rest.pulls.list({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ head: `${context.repo.owner}:testing`,
+ base: 'main'
+ });
+
+ if (existing.data.length > 0) {
+ console.log('PR already open:', existing.data[0].html_url);
+ core.setOutput('url', existing.data[0].html_url);
+ core.setOutput('already_existed', 'true');
+ return;
+ }
+
+ const mergeMsg = '${{ steps.merge.outputs.merge_msg }}';
+
+ const pr = await github.rest.pulls.create({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ title: mergeMsg,
+ body: [
+ '## Automated merge: `testing` → `main`',
+ '',
+ 'This PR was created automatically after `development` was merged into `testing`.',
+ '',
+ '### Pre-merge checklist',
+ '- [ ] All CI checks passing on this PR',
+ '- [ ] Manual review completed',
+ '- [ ] Version confirmed in `pyproject.toml` and `project/settings.py`',
+ '- [ ] `DEPLOYMENT.md` is up to date if infrastructure changed',
+ '- [ ] No breaking changes to the DB schema without a migration',
+ ].join('\n'),
+ head: 'testing',
+ base: 'main'
+ });
+
+ console.log('Created PR:', pr.data.html_url);
+ core.setOutput('url', pr.data.html_url);
+ core.setOutput('already_existed', 'false');
+
+ - name: Notify owners by email
+ if: steps.merge.conclusion == 'success'
+ uses: dawidd6/action-send-mail@v3
+ with:
+ server_address: smtp.gmail.com
+ server_port: 465
+ secure: true
+ username: ${{ secrets.EMAIL_HOST_USER }}
+ password: ${{ secrets.EMAIL_SECRET }}
+ from: "Five a Day CI <${{ secrets.EMAIL_HOST_USER }}>"
+ to: ${{ secrets.OWNER_EMAILS }}
+ subject: "[Five a Day] development → testing merged — PR awaiting review"
+ html_body: |
+ development → testing merged
+ A new merge has landed on the testing branch and a pull request to main is now awaiting review.
+
+
+ | Merge commit | ${{ steps.merge.outputs.merge_msg }} |
+ | Repository | ${{ github.repository }} |
+ | PR status | ${{ steps.pr.outputs.already_existed == 'true' && 'Updated (PR already existed)' || 'Newly created' }} |
+
+
+
+ Review PR →
+
+
+
+
+ Sent automatically by the auto-merge workflow. The PR cannot be merged to main until all CI checks pass and a code owner approves.
+
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..c5ed88c
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,127 @@
+name: CI
+
+on:
+ push:
+ branches: [development, testing, main]
+ pull_request:
+ branches: [testing, main]
+
+# Cancel in-progress runs on the same branch when new commits arrive
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ # ============================================================
+ # JOB 1: Lint — Ruff (fast, no DB needed)
+ # ============================================================
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v5
+ with:
+ enable-cache: true
+
+ - name: Install dependencies
+ run: uv sync --frozen --no-install-project
+
+ - name: Ruff — check
+ run: uv run --no-project ruff check project/
+
+ - name: Ruff — format
+ run: uv run --no-project ruff format --check project/
+
+ - name: Bandit — security linter
+ run: PYTHONUTF8=1 uv run bandit -r project/ -c pyproject.toml
+
+ # ============================================================
+ # JOB 2: Type check — mypy (no DB needed)
+ # ============================================================
+ typecheck:
+ name: Type check
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v5
+ with:
+ enable-cache: true
+
+ - name: Install dependencies
+ run: uv sync --frozen --no-install-project
+
+ - name: mypy
+ run: uv run mypy project/
+
+ # ============================================================
+ # JOB 3: Tests — pytest against PostgreSQL
+ # ============================================================
+ test:
+ name: Tests
+ runs-on: ubuntu-latest
+
+ services:
+ postgres:
+ image: postgres:16
+ env:
+ POSTGRES_DB: fiveaday_test
+ POSTGRES_USER: fiveaday_user
+ POSTGRES_PASSWORD: fiveaday_test_pw
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v5
+ with:
+ enable-cache: true
+
+ - name: Install dependencies
+ run: uv sync --frozen --no-install-project
+
+ - name: Run tests
+ working-directory: project
+ env:
+ DJANGO_SETTINGS_MODULE: project.settings_test
+ POSTGRES_DB: fiveaday_test
+ POSTGRES_USER: fiveaday_user
+ POSTGRES_PASSWORD: fiveaday_test_pw
+ TEST_DB_HOST: localhost
+ DJANGO_SECRET_KEY: ci-test-secret-key-not-for-production
+ LOGIN_USERNAME: ci_admin
+ LOGIN_PASSWORD: ci_test_password
+ DJANGO_ALLOWED_HOSTS: localhost,127.0.0.1
+ run: |
+ uv run python -m pytest tests/ -v --tb=short -n auto \
+ --cov=core --cov=students --cov=billing --cov=comms \
+ --cov-report=xml --cov-report=term-missing
+
+ - name: Upload coverage to Codecov
+ if: always()
+ uses: codecov/codecov-action@v4
+ with:
+ files: project/coverage.xml
+ fail_ci_if_error: false
+ token: ${{ secrets.CODECOV_TOKEN }}
+
+ - name: Upload coverage artifact
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-report
+ path: project/coverage.xml
+ retention-days: 7
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..ffdabf3
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,36 @@
+name: CodeQL Security Analysis
+
+on:
+ push:
+ branches: [main, testing, development]
+ pull_request:
+ branches: [main]
+ schedule:
+ - cron: '30 4 * * 1' # Every Monday at 04:30 UTC
+
+permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+jobs:
+ analyze:
+ name: Analyze Python
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: python
+ queries: security-and-quality
+
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v3
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: /language:python
diff --git a/.github/workflows/notify-production.yml b/.github/workflows/notify-production.yml
new file mode 100644
index 0000000..8a687e9
--- /dev/null
+++ b/.github/workflows/notify-production.yml
@@ -0,0 +1,81 @@
+name: Notify production
+
+# Fires whenever a new commit lands on main (production). Sends an email to
+# hellofiveaday@gmail.com so the team knows a production deploy is ready.
+
+on:
+ push:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ notify:
+ name: Send production notification
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 2 # Need HEAD and HEAD~1 for diff summary
+
+ - name: Gather commit info
+ id: info
+ run: |
+ SHA=${{ github.sha }}
+ SHORT_SHA=$(git rev-parse --short "$SHA")
+ MESSAGE=$(git log -1 --format="%s" "$SHA")
+ AUTHOR=$(git log -1 --format="%an" "$SHA")
+ FILES_CHANGED=$(git diff --stat HEAD~1 HEAD | tail -1 || echo "unknown")
+
+ {
+ echo "short_sha=$SHORT_SHA"
+ echo "message=$MESSAGE"
+ echo "author=$AUTHOR"
+ echo "files_changed=$FILES_CHANGED"
+ } >> "$GITHUB_OUTPUT"
+
+ - name: Send email
+ uses: dawidd6/action-send-mail@v3
+ with:
+ server_address: smtp.gmail.com
+ server_port: 465
+ secure: true
+ username: ${{ secrets.EMAIL_HOST_USER }}
+ password: ${{ secrets.EMAIL_SECRET }}
+ from: "Five a Day CI <${{ secrets.EMAIL_HOST_USER }}>"
+ to: hellofiveaday@gmail.com
+ subject: "[Five a Day] New commit on main — production deploy ready"
+ html_body: |
+ New commit on main (production)
+ A new commit has landed on the main branch. Production deploy is ready when you are.
+
+
+ | Commit | ${{ steps.info.outputs.message }} |
+ | Author | ${{ steps.info.outputs.author }} |
+ | SHA | ${{ steps.info.outputs.short_sha }} |
+ | Files changed | ${{ steps.info.outputs.files_changed }} |
+ | Repository | ${{ github.repository }} |
+
+
+
+ View commit →
+
+
+
+ Next steps
+
+ - Verify CI is green on this commit
+ - Deploy to Cloud Run:
+
gcloud builds submit --tag $IMAGE .
+ gcloud run deploy fiveaday --image=$IMAGE --region=europe-southwest1
+
+ - Run migrations if the schema changed:
+
gcloud run jobs execute fiveaday-migrate --region=europe-southwest1 --wait
+
+
+
+
+ Sent automatically by the notify-production workflow on every push to main.
+
diff --git a/project/project/settings.py b/project/project/settings.py
index f8508c6..6c149b4 100644
--- a/project/project/settings.py
+++ b/project/project/settings.py
@@ -15,7 +15,7 @@
# NOTA: Al cambiar la versión, actualizar también en:
# - readme.md (badge y texto)
# - pyproject.toml (campo version)
-APP_VERSION = os.getenv("APP_VERSION", "1.0.4")
+APP_VERSION = os.getenv("APP_VERSION", "1.0.5")
# ============================================================================
# SECURITY SETTINGS
diff --git a/pyproject.toml b/pyproject.toml
index 5dc29f2..3d78a11 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "five-a-day"
-version = "1.0.4"
+version = "1.0.5"
description = "Five a Day management software"
readme = "README.md"
requires-python = ">=3.12"
diff --git a/readme.md b/readme.md
index 996ff73..0856185 100644
--- a/readme.md
+++ b/readme.md
@@ -9,7 +9,7 @@
-
+
@@ -31,17 +31,20 @@ Built to centralize student records, automate billing cycles, and streamline par
| Environment | Version | Status |
|-------------|---------|--------|
-| **Production** | v0.0.0 |  |
-| **Testing (QA)** | v1.0.1t |  |
-| **Development** | v1.0.0 |  |
+| **Production** | v1.0.4 |  |
+| **Testing (QA)** | v1.0.4 |  |
+| **Development** | v1.0.4 |  |
| | |
|---|---|
-| **Documentation** | This README, [DEPLOYMENT.md](DEPLOYMENT.md), [HTTPS.md](docs/HTTPS.md), [UV.md](docs/UV.md), per-app READMEs, [CLAUDE.md](CLAUDE.md) |
+| **Documentation** | This README, [DEPLOYMENT.md](DEPLOYMENT.md), [GITHUB.md](docs/GITHUB.md), [HTTPS.md](docs/HTTPS.md), [UV.md](docs/UV.md), [CELERY.md](docs/CELERY.md), per-app READMEs, [CLAUDE.md](CLAUDE.md) |
| Version | Date | Description |
|---------|------|-------------|
-| **v1.0.1t** | 2026-04-14 | QA/testing environment: `/testing/` dashboard, database seeding, backlog with email, error reporting, HTTPS guide, access control via `QA_TESTING_USERNAME` |
+| **v1.0.4** | 2026-04-15 | GitHub Actions CI/CD pipeline (lint, typecheck, tests, CodeQL, Dependabot), auto-merge `development` → `testing` with 24h delay + auto-PR to `main`, email notifications, branch protection rules, inspirational quote generator, GCP deployment plan, cleaned legacy Render config, `make version x.y.z` + `make pc-run` with auto version bump |
+| v1.0.3 | 2026-04-14 | Test coverage raised to 70% — 294 tests total (40+ new tests) across auth views, comms services, dashboard, schedule, management, and apps modules |
+| v1.0.2 | 2026-04-13 | Replaced Poetry with UV, full developer tooling (Ruff, mypy, bandit, pip-audit, pre-commit, pytest-cov, pytest-xdist, pytest-randomly) |
+| v1.0.1t | 2026-04-14 | QA/testing environment: `/testing/` dashboard, database seeding, backlog with email, error reporting, HTTPS guide, access control via `QA_TESTING_USERNAME` |
| v1.0.0 | 2026-04-11 | Security hardening, query optimization (Case/When aggregates, N+1 fixes), GCP config, transaction safety |
| v1.0.0 | 2026-04-10 | Multi-app architecture, service layer, 132 tests, frontend cleanup, full documentation |
| v0.30.2 | 2025-03-14 | History system, GDPR for adults, Docker Compose workflow |
@@ -133,6 +136,18 @@ Built to centralize student records, automate billing cycles, and streamline par
- [For developers: how the QA environment works](#for-developers-how-the-qa-environment-works)
- [Access control for /testing/](#access-control-for-testing)
- [GCP deployment plan](#gcp-deployment-plan)
+ - [CI/CD \& GitHub Actions](#cicd--github-actions)
+ - [Pipeline Overview](#pipeline-overview)
+ - [Branch Strategy](#branch-strategy)
+ - [Workflows](#workflows)
+ - [Automated Flows](#automated-flows)
+ - [Branch Protection — `main`](#branch-protection--main)
+ - [Branch Protection — `testing`](#branch-protection--testing)
+ - [Public Repository Hardening](#public-repository-hardening)
+ - [Required GitHub Secrets](#required-github-secrets)
+ - [Email Notifications](#email-notifications)
+ - [Dependabot](#dependabot)
+ - [CodeQL Security Scanning](#codeql-security-scanning)
- [Contributing](#contributing)
- [Development Workflow](#development-workflow)
- [Code Conventions](#code-conventions)
@@ -143,8 +158,86 @@ Built to centralize student records, automate billing cycles, and streamline par
## Version History & Roadmap
-
-v1.0.1t — QA Testing Environment (current, testing branch)
+
+v1.0.4 — CI/CD Pipeline, GCP Migration Plan, Quote Generator (current)
+
+**GitHub Actions CI/CD** (new — see [docs/GITHUB.md](docs/GITHUB.md))
+
+- `ci.yml` — three parallel jobs on every push/PR: Ruff + Bandit lint, mypy type check, pytest against PostgreSQL 16 service container with coverage uploaded to Codecov
+- `auto-merge.yml` — hourly cron that merges `development` → `testing` after 24h of inactivity and CI passing, then auto-creates a PR `testing` → `main`
+- `codeql.yml` — weekly Python security analysis (OWASP Top 10, Django-specific queries)
+- `notify-production.yml` — emails `hellofiveaday@gmail.com` on every push to `main` with commit info and deploy instructions
+- Owner email notifications when `development` → `testing` merge lands + PR opened to `main`
+- `dependabot.yml` — grouped weekly Python and GitHub Actions updates targeting `development`
+- `CODEOWNERS` — auto-request reviews from both owner accounts
+
+**GCP migration plan** (new — see [DEPLOYMENT.md](DEPLOYMENT.md))
+
+- Full Cloud Run + Cloud SQL architecture documented
+- Three environments: local Docker (dev), Compute Engine e2-micro free tier (testing), Cloud Run + Cloud SQL (production)
+- Cost estimate: ~$15-27/month for production, $0/month for testing
+- Celery replacement strategy using Cloud Scheduler + Cloud Run Jobs
+- Cleaned legacy Render config (`render.yaml` removed, commented nginx and pgAdmin services removed from docker-compose)
+
+**Dashboard enhancement**
+
+- Inspirational quote generator on `/home` — fetches two daily quotes from `zenquotes.io`, stores them in a 48h cookie, rotates daily (day 0 shows quote 1, day 1+ shows quote 2), graceful fallback to the default Spanish subtitle on API failure
+
+**Developer tooling**
+
+- `make version x.y.z` — positional argument (replaces `V=x.y.z`) with confirmation guard before writing
+- `make pc-run` — renamed from `pre-commit-run`; after a clean pass, prompts to auto-increment the patch version in `pyproject.toml` and `project/settings.py`
+
+**Bug fixes**
+
+- Celery worker and beat containers added to docker-compose with correct permissions and health checks
+- Several payment and enrollment issues fixed
+
+
+
+
+v1.0.3 — Test Coverage Expansion (70%)
+
+**Testing**
+
+- Test count raised from 252 to **294** (40+ new tests)
+- Coverage raised to **70%** across `core`, `students`, `billing`, `comms`
+- New test files: `test_auth_views.py`, `test_comms_services.py`, `test_dashboard.py`, `test_schedule_views.py`, `test_management_views.py`, `test_app_forms.py`
+- Additional parametrized test cases for email-form views and error pages
+
+**Coverage tooling**
+
+- `coverage.svg` badge now reflects the improved coverage
+- `make coverage-badge` command streamlines badge regeneration
+
+
+
+
+v1.0.2 — UV Migration & Developer Tooling
+
+**Dependency management**
+
+- Replaced Poetry with UV (see [docs/UV.md](docs/UV.md))
+- `uv.lock` replaces `poetry.lock`
+- All Make commands updated to use `uv run`
+
+**Developer tooling**
+
+- **Ruff** — unified lint + format (replaces flake8, isort, black)
+- **mypy** with `django-stubs` — static type checking
+- **bandit** — Python security linter
+- **pip-audit** — dependency CVE scanning
+- **pytest-xdist** — parallel test execution (`-n auto`)
+- **pytest-randomly** — randomized test order with reproducible seeds
+- **pytest-cov** — coverage reports (HTML + XML + terminal)
+- **pre-commit** hooks — Ruff, mypy, bandit on every commit
+
+All tools configured in `pyproject.toml` — single source of truth.
+
+
+
+
+v1.0.1t — QA Testing Environment
**Testing infrastructure**
- QA Docker Compose overlay (`docker-compose.testing.yml`) — Gunicorn, `DEBUG=False`, separate DB volume
@@ -1433,6 +1526,196 @@ After deployment, Cloud Run provides a URL like `https://fiveaday-testing-xxxxx.
---
+## CI/CD & GitHub Actions
+
+The project runs a fully automated CI/CD pipeline on GitHub Actions. Every push is tested, every merge is audited, and production is reached only through a protected pull request. The full configuration reference is in [docs/GITHUB.md](docs/GITHUB.md) — this section is the overview.
+
+### Pipeline Overview
+
+```text
+Push to development
+ │
+ ▼
+CI runs (lint + typecheck + tests) + CodeQL
+ │
+ │ hourly cron
+ ▼
+Auto-merge check
+ • development ahead of testing?
+ • last commit ≥ 24 h old?
+ • CI passing on that commit?
+ │ all yes
+ ▼
+git merge development → testing
+(commit: "YYYY-MM-DD - ")
+ │
+ ├── CI re-runs on testing
+ └── PR created: testing → main
+ │
+ ▼
+Email to owners (OWNER_EMAILS)
+ │
+ ▼
+Manual review + Code Owner approval
+ │
+ ▼
+Merge to main (protected — all checks required)
+ │
+ ▼
+Email to hellofiveaday@gmail.com
+(production deploy ready)
+```
+
+### Branch Strategy
+
+| Branch | Purpose | Protected | Direct push |
+|---------------|------------------------------------------|--------------------------|--------------------------|
+| `main` | Production. Every commit is deployable. | Full protection | No (PR + review only) |
+| `testing` | Staging. Auto-merged from development. | Minimal (no force/delete)| Only from auto-merge flow|
+| `development` | Active development. Day-to-day work. | None | Yes |
+
+Feature branches off `development` are welcome for non-trivial work, but the expected flow is: work on `development` → wait 24 h → auto-promoted to `testing` → manual merge to `main`.
+
+### Workflows
+
+| Workflow | File | Triggers | Purpose |
+|----------|------|----------|---------|
+| **CI** | [`ci.yml`](.github/workflows/ci.yml) | Push to `development`/`testing`/`main`; PRs to `testing`/`main` | Three parallel jobs — **Lint** (Ruff + Bandit), **Type check** (mypy), **Tests** (pytest + PostgreSQL 16 service container + Codecov upload) |
+| **Auto-merge** | [`auto-merge.yml`](.github/workflows/auto-merge.yml) | Hourly cron + manual dispatch | Merges `development` → `testing` when conditions pass, creates PR to `main`, emails owners |
+| **CodeQL** | [`codeql.yml`](.github/workflows/codeql.yml) | Push to `main`/`testing`/`development`; PRs to `main`; Monday 04:30 UTC | Python static security analysis (OWASP Top 10, Django-specific queries) |
+| **Notify production** | [`notify-production.yml`](.github/workflows/notify-production.yml) | Push to `main` | Emails `hellofiveaday@gmail.com` with commit info and `gcloud` deploy instructions |
+| **Dependabot** | [`dependabot.yml`](.github/dependabot.yml) | Weekly (Mondays 08:00 Madrid) | Grouped Python and GitHub Actions updates targeting `development` |
+
+Concurrent CI runs on the same branch cancel each other automatically — new pushes always produce a fresh run.
+
+### Automated Flows
+
+**1. You push to `development`**
+
+- CI triggers immediately (lint, typecheck, tests run in parallel, ~2-4 min)
+- CodeQL triggers immediately (weekly scan also runs independently)
+- The hourly auto-merge cron checks this commit every hour until it is ≥ 24 h old with passing CI, then promotes to `testing`
+
+**2. Auto-merge fires**
+
+- Creates a `--no-ff` merge commit on `testing` titled `YYYY-MM-DD - `
+- Pushes to `testing` (which triggers CI on `testing`)
+- Opens PR `testing → main` if one is not already open (title matches the merge commit)
+- Sends an HTML email to `OWNER_EMAILS` with a "Review PR" button
+
+**3. You review and merge the PR**
+
+- All required checks must pass (Lint, Type check, Tests, CodeQL alerts, Code Owner approval)
+- You cannot approve your own PR — the second owner account approves
+- On merge, `main` is updated
+
+**4. Production notification fires**
+
+- `notify-production.yml` sends an email to `hellofiveaday@gmail.com`
+- Email contains commit info, file-change summary, and the exact `gcloud` commands to deploy to Cloud Run
+
+### Branch Protection — `main`
+
+Configure at **Settings → Branches → Add ruleset**, target `main`:
+
+**Required status checks** (names must match CI job names exactly):
+
+| Check | Workflow |
+|-------|----------|
+| `Lint` | ci.yml |
+| `Type check` | ci.yml |
+| `Tests` | ci.yml |
+| `Analyze Python` | codeql.yml |
+
+**Protection rules** (every item below enabled):
+
+| Rule | Setting |
+|------|---------|
+| Require a pull request before merging | ✓ |
+| Required approvals | **1** (higher if you add collaborators) |
+| Dismiss stale reviews when new commits are pushed | ✓ |
+| Require review from Code Owners | ✓ |
+| Require status checks to pass | ✓ |
+| Require branches to be up to date before merging | ✓ |
+| Require conversation resolution before merging | ✓ |
+| Require signed commits | ✓ (strongly recommended for a public repo) |
+| Require linear history | ✓ (enforces squash/rebase merges) |
+| Restrict who can push to matching branches | ✓ |
+| Do not allow bypassing the above settings | ✓ (admins follow the same rules) |
+| Allow force pushes | ✗ |
+| Allow deletions | ✗ |
+
+### Branch Protection — `testing`
+
+`testing` needs direct pushes from the auto-merge workflow, so PR requirements are **not** enforced. Apply only safety rails:
+
+| Rule | Setting |
+|------|---------|
+| Require a pull request before merging | ✗ |
+| Allow force pushes | ✗ |
+| Allow deletions | ✗ |
+| Require status checks to pass (optional) | ✓ — lets CI block a broken auto-merge from polluting `testing` further |
+
+### Public Repository Hardening
+
+Because this repository is **public**, extra care is taken to prevent accidental secret leaks, abuse of the CI, and unreviewed contributions:
+
+| Control | Where | Why |
+|---------|-------|-----|
+| **GitHub Secret Scanning** | Settings → Code security | Free for public repos — detects committed secrets across history |
+| **Push Protection** | Settings → Code security | Free for public repos — blocks pushes that contain secrets before they land |
+| **CodeQL** | `codeql.yml` + Settings → Code security | Free for public repos — weekly security analysis |
+| **Dependabot alerts + security updates** | Settings → Code security | Free for public repos — fixes known CVEs in dependencies |
+| **Require 2FA for all contributors** | Organization settings (if in an org) | Prevents compromised account pushes |
+| **Restrict fork PRs from running CI with secrets** | Settings → Actions → Fork PR workflows: require approval for first-time contributors | Prevents secret exfiltration via malicious PRs from forks |
+| **Actions allow-list** | Settings → Actions → Allow specific actions | Prevents supply-chain attacks — pin to verified creators only |
+| **Workflow permissions default: read-only** | Settings → Actions → Workflow permissions | Individual workflows explicitly request `write` where needed |
+| **Block workflows from approving PRs** | Settings → Actions → Allow GitHub Actions to create and approve pull requests: **only allow create, not approve** | Humans must approve, even automated PRs |
+| **SECURITY.md** | Root of the repo | Public disclosure policy so researchers know how to report vulnerabilities privately |
+| **License file** | Root of the repo | Required for a public repo — defines what others can legally do with the code |
+
+The `.env` file is gitignored and **never** committed. Production secrets live in GCP Secret Manager (see [DEPLOYMENT.md](DEPLOYMENT.md)), not in the repository or in GitHub Secrets. GitHub Secrets are used only for CI operations (sending notification emails, uploading coverage).
+
+### Required GitHub Secrets
+
+Configure at **Settings → Secrets and variables → Actions**:
+
+| Secret | Required by | Purpose |
+|--------|-------------|---------|
+| `GH_PAT` | auto-merge.yml | Fine-grained Personal Access Token. Pushes to `testing` and creates PRs *while triggering downstream CI* (which the default `GITHUB_TOKEN` cannot do). Permissions: Contents RW, Pull requests RW, Checks R, Metadata R |
+| `EMAIL_HOST_USER` | auto-merge.yml, notify-production.yml | Gmail address used to send notification emails |
+| `EMAIL_SECRET` | auto-merge.yml, notify-production.yml | Gmail App Password — can be the same one the application uses for transactional email |
+| `OWNER_EMAILS` | auto-merge.yml | Comma-separated recipient list for the `development → testing` merge notification |
+| `CODECOV_TOKEN` | ci.yml | Optional — only needed for private repos. Public repos push coverage anonymously |
+
+**Rotate `GH_PAT` annually.** Without it, the auto-merge falls back to the default `GITHUB_TOKEN`, which cannot trigger CI on PRs it creates — breaking the pipeline silently.
+
+### Email Notifications
+
+| Event | Recipient | Sent by |
+|-------|-----------|---------|
+| `development → testing` merged + PR opened to `main` | `OWNER_EMAILS` (secret) | auto-merge.yml |
+| New commit on `main` (production ready to deploy) | `hellofiveaday@gmail.com` (hardcoded) | notify-production.yml |
+
+Both use Gmail SMTP via the `dawidd6/action-send-mail@v3` action. Emails include HTML formatting, links to the commit/PR, and actionable next steps.
+
+### Dependabot
+
+Dependabot opens **weekly PRs on `development`** (Mondays, 08:00 Europe/Madrid) for:
+
+- **Python packages** — minor and patch updates grouped into a single PR. Django major version bumps are intentionally ignored (require manual upgrade planning).
+- **GitHub Actions** — updates to `actions/*`, `astral-sh/setup-uv`, `dawidd6/action-send-mail`, etc.
+
+PRs are labelled `dependencies` + `python` or `github-actions` for easy filtering. The normal 24 h cycle carries merged updates to `testing` and then to `main`.
+
+### CodeQL Security Scanning
+
+Runs on every push and PR to `main`, plus a full scan every Monday at 04:30 UTC. Uses the `security-and-quality` query suite — covers OWASP Top 10, CWE Top 25, and Django-specific queries (SQL injection, path traversal, hardcoded credentials, insecure deserialization, etc.).
+
+Results appear in **Security → Code scanning alerts**. A new alert on `main` does not auto-block future merges unless branch protection is configured to require the CodeQL check.
+
+---
+
## Contributing
### Development Workflow
diff --git a/uv.lock b/uv.lock
index a0e1530..ba23172 100644
--- a/uv.lock
+++ b/uv.lock
@@ -695,7 +695,7 @@ wheels = [
[[package]]
name = "five-a-day"
-version = "1.0.4"
+version = "1.0.5"
source = { editable = "." }
dependencies = [
{ name = "celery" },
From b43651543524a948b6d66619ebe736251319beb5 Mon Sep 17 00:00:00 2001
From: Joaquin Hernandez Martinez
Date: Wed, 15 Apr 2026 14:36:22 +0200
Subject: [PATCH 2/8] v1.0.6 - Added documentation skill for agentic
documentation and reviewed current documentation (updating most of it), and
fixed CI/CD pipeline, adding aditional logic
---
.claude/skills/update-readme/SKILL.md | 460 ++++++++++++++++++++++++
.env.testing.example | 92 -----
.github/workflows/auto-merge.yml | 81 ++++-
.github/workflows/notify-production.yml | 40 ++-
.gitignore | 7 +-
CLAUDE.md | 24 +-
DEPLOYMENT.md | 3 +-
Makefile | 10 +-
readme.md => README.md | 400 +++++++++++++--------
project/comms/README.md | 8 +-
project/core/README.md | 19 +-
project/project/settings.py | 6 +-
pyproject.toml | 2 +-
uv.lock | 2 +-
14 files changed, 880 insertions(+), 274 deletions(-)
create mode 100644 .claude/skills/update-readme/SKILL.md
delete mode 100644 .env.testing.example
rename readme.md => README.md (81%)
diff --git a/.claude/skills/update-readme/SKILL.md b/.claude/skills/update-readme/SKILL.md
new file mode 100644
index 0000000..e8602a3
--- /dev/null
+++ b/.claude/skills/update-readme/SKILL.md
@@ -0,0 +1,460 @@
+---
+name: update-readme
+description: Use when the user says their work is done and they want the project documentation updated to reflect the staged changes. Despite the name, this skill updates the full documentation set — the top-level README.md, every per-app README, CLAUDE.md, DEPLOYMENT.md, and every file under docs/. Inspects the staged diff, routes changes to the correct docs, applies per-file checklists, and sweeps for stale references across all of them.
+---
+
+# update-readme
+
+The user invokes this skill when they have finished a unit of work and want the documentation brought up to date with what's in the staged area.
+
+**Scope.** This skill touches EVERY documentation file in the repo — not just `README.md`. The name is historical; treat it as `update-docs`. The documentation set is:
+
+| File | Authoritative for |
+|------|-------------------|
+| `README.md` (root) | Project overview, status, version history, tech stack, directory layout, testing, security, CI/CD, QA, contributing |
+| `CLAUDE.md` | Rules and conventions future AI agents must follow. Gotchas. README-maintenance checklist. |
+| `DEPLOYMENT.md` | GCP deployment — all 3 environments, costs, commands, free tier notes |
+| `docs/GITHUB.md` | Full CI/CD reference — workflows, branch protection, secrets, public-repo hardening |
+| `docs/HTTPS.md` | HTTPS setup (Docker Nginx + Cloud Run) |
+| `docs/UV.md` | UV package manager guide |
+| `docs/CELERY.md` | Celery worker/beat reference |
+| `docs/TODO.md` | Open tasks log |
+| `project/core/README.md` | Models, views, URLs, middleware, templates/static for the `core` app |
+| `project/students/README.md` | Models, forms, admin, URLs for the `students` app |
+| `project/billing/README.md` | Models, services, constants, exports, admin, URLs for the `billing` app |
+| `project/comms/README.md` | EmailService, email functions, Celery tasks, management commands for `comms` |
+| `project/ENROLLMENT_PAYMENT_SYSTEM.md` | Canonical enrollment/payment business rules (pricing, discounts, schedule) |
+
+**`.env.example` does not exist in this repo — do not create it.** The `.env` template lives inline in `README.md` under the heading **`.env template`** (a fenced `bash` code block inside the Development & Docker section). Every time you edit the app's env var surface, update that code block.
+
+Everything under `.venv/`, `.pytest_cache/`, `node_modules/`, `.git/` is **out of scope**. You **may read `.env`** solely to extract variable names and section comments when the user explicitly asks you to refresh the README's `.env template` code block — strip every value before writing anything user-visible. Never read `.env.testing` or any other `.env*`.
+
+---
+
+## Step 1 — Inspect what changed
+
+Do not guess. Run these first and read the output:
+
+```bash
+git status --porcelain # staged + unstaged
+git diff --cached --stat # summary of staged changes
+git diff --cached # full staged diff (use Grep on it if very large)
+git log -10 --oneline # recent commits for version/date context
+git log HEAD --format='%s%n%b' -1 # full current commit message (if any)
+grep '^version' pyproject.toml # current version from pyproject
+grep 'APP_VERSION' project/project/settings.py | head -1 # fallback default
+git remote -v # owner/repo for CI badges
+```
+
+If the working tree has **unstaged** changes that look relevant, ask the user whether to include them. Never stage files yourself without confirmation.
+
+---
+
+## Step 2 — Route staged files to the right docs
+
+For every staged path, the table below tells you which docs are candidates for updates. A single staged change may touch many docs; apply the union.
+
+| Staged path pattern | Docs to review |
+|---------------------|----------------|
+| `project/core/models.py`, `project/core/views/**`, `project/core/middleware.py`, `project/core/templates/**`, `project/core/static/**`, `project/core/decorators.py`, `project/core/context_processors.py` | `project/core/README.md` **+** main README (Architecture, Directory Layout, Features by View, Design Decisions) |
+| `project/students/**/*.py` (models, forms, admin, urls) | `project/students/README.md` **+** main README (ER diagram if models changed, Directory Layout) |
+| `project/billing/models.py`, `project/billing/services/**`, `project/billing/constants.py`, `project/billing/exports.py`, `project/billing/admin.py`, `project/billing/urls.py` | `project/billing/README.md` **+** main README (Database Schema, Features by View → Payments) **+** `project/ENROLLMENT_PAYMENT_SYSTEM.md` if pricing, discounts, or enrollment rules changed |
+| `project/comms/services/**`, `project/comms/tasks.py`, `project/comms/management/commands/**`, `project/comms/urls.py` | `project/comms/README.md` **+** main README (Features by View → Apps) |
+| `project/tests/**` | Main README (Testing section — test counts, per-file tables) **+** the app README whose logic the new test covers |
+| `project/project/settings*.py` | Main README (Env Variables Reference, `.env template` code block, App Versioning) **+** `DEPLOYMENT.md` if a new env var |
+| Any other Python file using `os.getenv(...)` | Main README (Env Variables Reference + `.env template` code block) **+** `DEPLOYMENT.md` Secret Manager list if the var carries a secret |
+| `Makefile` | Main README (Make Commands table, Contributing → Make Commands Developer Tooling) **+** `CLAUDE.md` Gotchas if a command was renamed or removed |
+| `pyproject.toml` | Main README (Tech Stack → Python Dependencies, Developer Tooling) **+** `docs/UV.md` if tooling-related **+** version badge and tables if version bumped |
+| `uv.lock` | Usually no doc change needed (lock-file churn) — but confirm no version mismatch |
+| `Dockerfile`, `docker-compose*.yml`, `entrypoint.sh` | Main README (Development & Docker, Testing Environment) **+** `DEPLOYMENT.md` if production image layout changed |
+| `.github/workflows/**` | Main README (CI/CD section) **+** `docs/GITHUB.md` (full reference) |
+| `.github/dependabot.yml`, `.github/CODEOWNERS` | Main README (CI/CD section) **+** `docs/GITHUB.md` |
+| `docs/HTTPS.md` | Main README (Security → Transport, Testing Environment) |
+| `docs/UV.md` | Main README (Tech Stack, Contributing) |
+| `docs/CELERY.md` | Main README (Tech Stack), `project/comms/README.md` |
+| `docs/GITHUB.md` | Main README CI/CD section (keep them in sync — README is the overview, GITHUB.md is the reference) |
+| `DEPLOYMENT.md` | Main README (Project Status → Hosting column, GCP references) |
+| `CLAUDE.md` | No downstream doc changes — but re-read it to confirm your edits don't contradict the rules |
+
+---
+
+## Step 3 — Per-file detailed checklists
+
+### 3.1 — `README.md` (root)
+
+Work through this list in order. **Do not skip a step just because nothing "looks" staged for it** — the last step greps for stale content and will catch anything the routing table missed.
+
+#### a. Header badges (≈lines 11-18)
+
+- Version badge must equal `pyproject.toml`'s version
+- CI and Codecov badges must use the owner/repo from `git remote -v`
+- Don't bloat the header — no more badges than the originals + any new CI workflow
+
+#### b. Project Status table — **three rows, exact order: Production → Testing → Development**
+
+| Environment | Branch | Hosting | CI Status |
+|-------------|--------|---------|-----------|
+| **Production** | `main` | GCP Cloud Run + Cloud SQL (PostgreSQL 16), `europe-southwest1` | CI badge for `main` |
+| **Testing (QA)** | `testing` | GCP Compute Engine e2-micro (free tier), Docker Compose | CI badge for `testing` |
+| **Development** | `development` | Local machine via `make up` (Docker Compose) | CI badge for `development` |
+
+If hosting changes (region, new managed service, etc.) update the Hosting cell. Never reorder rows. Never add rows.
+
+#### c. Recent Versions table — **exactly 3 rows**
+
+- Current version + the two previous patches
+- When adding a new version, **delete the oldest row**
+- Date in `YYYY-MM-DD`
+- Description: dense one-liner that names every user-visible change in that version. Synthesise — don't copy commit subjects verbatim. Distill from `git log ..HEAD --format='%s%n%b'` plus the staged diff.
+
+#### d. Version History `` blocks
+
+- Add a new `` at the top of the section
+- **Remove** the `open` attribute from the previously-open block
+- Content structure must match the existing blocks: `**Subsection**` bold headings + bullet lists. Typical subsections: GitHub Actions CI/CD, GCP migration, Dashboard enhancement, Developer tooling, Testing, Bug fixes, Public-repo hardening. Only include subsections that are actually relevant to what changed.
+- Older blocks stay unless they contradict current state
+
+#### e. Roadmap
+
+If a roadmap item shipped, move its content to the Version History block for the shipping version and **remove** the roadmap entry. Do not leave completed items in the roadmap.
+
+#### f. Tech Stack
+
+- If `pyproject.toml` added/removed a package, update the Python Dependencies table
+- If a new developer tool was adopted (e.g. detect-secrets, commitizen), add it to Developer Tooling
+
+#### g. Database Schema
+
+- If any `project/*/models.py` changed, compare fields to the Mermaid ER diagram and the Key Constraints table
+- Model renames, new fields, deleted fields, new indexes, new UniqueConstraints — all must be reflected
+
+#### h. Development & Docker
+
+- `make help` command count changed? Update "60+ commands" (check with `grep -c '^[a-z].*:$' Makefile`)
+- Quick Start commands still valid?
+- Env Variables Reference: any new `os.getenv(...)` in `settings.py` → add a row
+- App Versioning paragraph reflects the current `make version` syntax
+
+#### i. Project Structure & Architecture → Directory Layout
+
+Update every line that's wrong:
+
+- `tests/` line: `pytest suite (N tests, X% coverage)` — get N from `grep -r "^def test_" project/tests/ | wc -l` and X from the latest coverage report (or Codecov)
+- `core/views/` annotation: view module count must be accurate
+- `Makefile` line: command count
+- New top-level files or directories (e.g. `.github/`, `docs/`, `scripts/`)
+- Deleted files/directories (e.g. `render.yaml`)
+
+Also refresh the App: core/students/billing/comms summary tables if models, view modules, or URL counts changed.
+
+#### j. Features by View
+
+- If a view was added or renamed, add/update the section
+- If a feature was removed from a page, remove its bullet
+
+#### k. Testing
+
+- Total test count must match `grep -r "^def test_" project/tests/ | wc -l`
+- Coverage % must match the current Codecov or local coverage report
+- Per-file test tables: test counts per file must match
+
+#### l. Security
+
+- If a deployment platform was removed (e.g. Render), delete every row, subsection, and mention. **Always** run `grep -in 'render\|gcp-cloudrun'` on the entire README before claiming it's clean.
+- If a security protection is now enabled (e.g. GitHub secret scanning, push protection), **move** its row from "Future Security Improvements" to the relevant active table.
+- If `SECURE_*` settings changed, update the Transport Security and Security Headers tables.
+
+#### m. Testing Environment (QA)
+
+- QA access instructions still accurate?
+- Any env var renames in `.env.testing`?
+- The GCP deployment plan is **gone** from this section — it lives only in `DEPLOYMENT.md` now. If it reappears, delete it.
+
+#### n. CI/CD & GitHub Actions
+
+- Workflows table: one row per `.github/workflows/*.yml` file
+- Pipeline Overview diagram: update if flow steps changed
+- Branch Protection tables: match what's actually configured (or what the user wants configured)
+- Public Repository Hardening: a new protection toggle → a new row
+- Required GitHub Secrets: every secret referenced in any workflow `${{ secrets.X }}` must appear in this table. Grep: `grep -rh 'secrets\.' .github/workflows/ | sort -u`.
+- Email Notifications: one row per notification email sent by any workflow
+
+#### o. Contributing
+
+- Development Workflow numbered list: current `make` commands, current commit message convention
+- Make Commands (Developer Tooling) table: every tool in `pyproject.toml` dev-dependencies should have a row with its `make` command
+
+#### p. Table of Contents
+
+Every `##` and `###` and `####` heading in the body must have a corresponding ToC entry. GitHub anchor rules:
+
+- Lowercase
+- Replace spaces with `-`
+- Drop everything except `a-z 0-9 - _`
+- Escape `&` as `\&` (escaped only in the ToC link text, not in the anchor itself)
+
+Deleted headings → delete the ToC entry. New headings → add an entry at the correct nesting depth.
+
+#### q. Stale-reference grep (MUST run before finishing)
+
+For every file or feature removed this session, grep the entire README. Examples:
+
+```bash
+grep -n "render\|render\.yaml\|gcp-cloudrun" README.md # Render removed
+grep -n "pre-commit-run" README.md # renamed to pc-run
+grep -n "174 tests\|132 tests\|252 tests" README.md # old test counts
+grep -n "V=1" README.md # old `make version V=x.y.z` syntax
+grep -n "webcrumbs" README.md # CSS wrapper removed
+```
+
+Any hit → fix it.
+
+---
+
+### 3.2 — `CLAUDE.md`
+
+CLAUDE.md is the rulebook for future AI agents. Check if the staged diff introduces:
+
+- A **new convention** (e.g. "always use `transaction.atomic()` for multi-model writes") — add to "Django Best Practices" or the relevant section
+- A **new gotcha** (something non-obvious that tripped someone) — add to "Gotchas"
+- A **renamed command, file, or symbol** — update the reference (e.g. `pre-commit-run` → `pc-run`, `make version V=x` → `make version x`)
+- A **new tool or service** — add to "How to run" or "Developer tooling"
+- A **new app** (unlikely) — add to "4 Django apps"
+
+The README-maintenance checklist inside CLAUDE.md (section "README maintenance") must stay in sync with the detailed Step 3.1 above. If you add/remove a step in the skill, update CLAUDE.md too.
+
+---
+
+### 3.3 — `DEPLOYMENT.md`
+
+Check when the staged diff touches infrastructure, Docker, settings, secrets, or anything that would change a production deploy.
+
+- **Architecture diagram** — services still accurate?
+- **Environments at a Glance** table — Development / Testing / Production all still correct?
+- **Prerequisites** — any new required tool?
+- **Initial Setup commands** — new service to enable, new secret to create?
+- **Build & Deploy** — env-var list on the `gcloud run deploy` command must match every `os.getenv(...)` in `settings.py`. Missing env vars cause production bugs.
+- **Celery strategy** — if Celery tasks were added/removed/renamed, update the Cloud Scheduler + Cloud Run Jobs sections
+- **Custom domain** — only update if the domain mapping command changed
+- **Cost estimates** — if hosting assumptions changed (e.g. `db-f1-micro` → `db-custom-1-3840`), recalculate
+- **Optional Services for Future Evolution** — move an entry out of this section if it's now implemented
+
+---
+
+### 3.4 — `docs/GITHUB.md`
+
+This is the CI/CD reference. README has the overview; GITHUB.md has the full walkthrough. They must not contradict each other.
+
+- **Pipeline Overview diagram** — matches README's
+- **Every workflow** has its own subsection with: what it does, when it triggers, jobs, any quirks
+- **Branch Protection** subsections (main, testing): every rule and required status check listed
+- **Public Repository Hardening** — table matches README's
+- **Required GitHub Secrets** — every `${{ secrets.X }}` in any workflow appears here with: required-by workflow, purpose, creation instructions where non-obvious (e.g. `GH_PAT` fine-grained token scopes)
+- **Email Notifications** table — matches README's
+- **Dependabot + CodeQL** sections — accurate
+
+---
+
+### 3.5 — `docs/HTTPS.md`, `docs/UV.md`, `docs/CELERY.md`
+
+Update these only when the corresponding tool or process changed. These are narrow, focused docs.
+
+- `HTTPS.md`: if the Nginx config or the Cloud Run TLS setup changed
+- `UV.md`: if UV workflow commands, lock behaviour, or dependency groups changed
+- `CELERY.md`: if worker/beat setup, task queues, or Redis configuration changed
+
+---
+
+### 3.6 — `docs/TODO.md`
+
+If the staged work **completes** a TODO item, remove it from this file. If it creates new follow-up work, add an entry with context.
+
+---
+
+### 3.7 — Per-app READMEs (`project//README.md`)
+
+Each app README has the same structure. When the app's source changes, keep these in sync:
+
+| Section | Source of truth | How to verify |
+|---------|----------------|---------------|
+| **Models table** | `project//models.py` | Every model class must have a row with table name and purpose |
+| **Views table** (core only) | `project/core/views/*.py` | One row per view module, listing every view function |
+| **Service Layer** (billing only) | `project/billing/services/*.py` | Every service class + every public method must be documented |
+| **Forms** | `project//forms.py` | Every ModelForm/Form subclass listed |
+| **Admin** | `project//admin.py` | Every `register`ed model with custom admin behaviour |
+| **URLs** | `project//urls.py` | URL pattern count ("12 URL patterns"); mention each route group |
+| **Management commands** | `project//management/commands/*.py` | Every file listed with a one-line description |
+| **Celery tasks** (comms only) | `project/comms/tasks.py` | Every `@shared_task` listed with a one-line description |
+
+If a new model/view/service/form/command/task was added in the staged diff, its app README must mention it.
+
+---
+
+### 3.8 — `project/ENROLLMENT_PAYMENT_SYSTEM.md`
+
+This is the **authoritative spec** for pricing and billing rules. If the staged diff touches:
+
+- `project/billing/constants.py` (pricing seeds)
+- `project/billing/services/pricing_service.py`, `enrollment_service.py`, or `payment_service.py`
+- `SiteConfiguration` model fields
+- The enrollment flow or discount logic
+
+then this file must be re-checked. The prices, discounts, and rules here must match the code.
+
+---
+
+### 3.9 — The `.env template` code block in `README.md`
+
+`.env.example` **does not exist in this repo**. Instead, the README's Development & Docker section contains a `.env template` heading followed by a fenced `bash` code block that documents every variable the app reads. Contributors copy this block into a new `.env` file and fill in the blanks.
+
+This block is the authoritative env-var spec for local dev. The skill keeps it healthy.
+
+**Required checks:**
+
+1. **Code block exists and is findable** — the README must contain a heading literally `### .env template` followed by a fenced \`\`\`bash block. Verify with:
+
+ ```bash
+ grep -n "^### \.env template" README.md
+ ```
+
+ If missing, flag it immediately — the Quick Start links to `#env-template` and `make setup` tells users to find this block.
+
+2. **Every var has an empty or safe value — no real secrets.** When the user asks you to refresh this block from `.env`, you may read `.env` to extract variable names and section-comment structure, but:
+
+ - Every value slot must be **empty** (`KEY=`) **or a safe default** that's already public information.
+ - **Safe defaults** (OK to ship): `localhost`, `127.0.0.1`, `0.0.0.0`, `db`, `redis://redis:6379/0`, `fiveaday_db`, `fiveaday_user`, `5432`, `8000`, `True`/`False`, `development`/`production`/`testing`, `INFO`/`DEBUG`, `postgres`, `fiveaday` (the well-known legacy default username), `http://localhost:8000/auth/google/callback/`.
+ - **Never acceptable** — anything that looks like a real secret and must be stripped before writing:
+ - Random 50-char Django secret keys (anything with high-entropy symbols)
+ - `ghp_` / `ghs_` / `gho_` / `github_pat_` prefixes (GitHub tokens)
+ - `GOCSPX-` prefix (Google OAuth client secrets)
+ - 72-char base64 strings (Django's `get_random_secret_key()` output, `openssl rand -base64` output)
+ - 16-char lowercase-letter groups like `krqg zqeq kcxc onub` (Gmail App Passwords)
+ - Real email addresses of the maintainer or users (the maintainer knows theirs — the README should say `your-academy@gmail.com` or leave blank)
+ - Real IBANs, phone numbers, business details
+ - Real Google OAuth client IDs (start with digits then `-...-apps.googleusercontent.com`)
+
+ If you see any of these when reading `.env`, **strip them** before writing to the README. Flag to the user that their `.env` contained secrets you saw.
+
+3. **Coverage matches the code** — every env var the app actually reads must appear in the template. Compare:
+
+ ```bash
+ # Every env var referenced in Python code (most authoritative)
+ grep -rhoE 'os\.getenv\("[A-Z_]+"' project/ | sort -u | sed 's/os\.getenv("//'
+
+ # Every var in the README template
+ grep -E '^[A-Z_]+=' README.md | cut -d= -f1 | sort -u
+ ```
+
+ The set difference tells you what's missing (code but not template) or stale (template but no longer read). Both sides should be addressed — missing → add a line under the right section, stale → remove from the template.
+
+4. **Coverage matches the Env Variables Reference table** — every row in the README's **Environment Variables Reference** table (the `| Variable | Description |...` table) should also appear in the template code block, unless it is explicitly marked "advanced override" (vars like `SESSION_COOKIE_AGE`, `APP_VERSION`, `GOOGLE_ALLOWED_EMAIL` can be left out of the template as long as they're in the reference table).
+
+5. **No duplicate keys** in the template — inside the fenced block:
+
+ ```bash
+ awk '/^### \.env template/{flag=1; next} /^```bash/{capture=flag; next} /^```/{capture=0; flag=0} capture && /^[A-Z_]+=/ {sub(/=.*/,""); print}' README.md | sort | uniq -d
+ ```
+
+ must be empty.
+
+6. **Grouping and comments preserved** — the template uses `# ====...====` banner dividers before each section (DJANGO SETTINGS, LOGGING, DATABASE CONFIGURATION, SUPERUSER, EMAIL CONFIGURATION, CELERY / REDIS, AUTHENTICATION, GOOGLE OAUTH, ACADEMY BUSINESS INFO). A new var must be placed in the right section — never appended at the bottom.
+
+7. **`.gitignore` is correct** — should have `.env*` with **no** exceptions (no `!.env.example` line). Verify:
+
+ ```bash
+ grep -n '\.env' .gitignore
+ ```
+
+ If `!.env.example` or `!.env.testing.example` or any other `!.env*` exception exists, remove it.
+
+8. **No stale `.env.example` references anywhere** — grep the full docs tree and Makefile:
+
+ ```bash
+ grep -rn '\.env\.example\|\.env\.testing\.example' README.md CLAUDE.md DEPLOYMENT.md docs/ Makefile project/
+ ```
+
+ Any hit → remove or replace with a pointer to the README template.
+
+9. **Legacy / deprecated vars** — if `.env` contains a var that the app no longer reads (e.g. `VERSION=0.30.2` from an old release), do not add it to the template. Flag it to the user so they can clean their real `.env`.
+
+10. **ToC entry** — the ToC must include `- [.env template](#env-template)` under Development & Docker.
+
+---
+
+## Step 4 — Cross-file consistency sweep
+
+Certain facts appear in multiple documents. When they change, update **every** occurrence:
+
+| Fact | Files that reference it |
+|------|-------------------------|
+| **Current version (`x.y.z`)** | Main README header badge, Recent Versions top row, Version History latest ``, `pyproject.toml`, `project/project/settings.py` APP_VERSION default |
+| **Test count (`N tests`)** | Main README header (Project Status area if mentioned), Testing section top, Directory Layout `tests/` annotation, per-app READMEs if they cite a count |
+| **Coverage %** | Main README Codecov badge, Testing Overview table, Directory Layout |
+| **Make command count** | Main README Directory Layout (`60+ commands`), help text |
+| **Python version (`3.12+`)** | README badge, `pyproject.toml` `requires-python`, Dockerfile `FROM python:3.12-slim`, `[tool.ruff] target-version = "py312"` |
+| **Django version (`5.2`)** | README badge, `pyproject.toml` dependency |
+| **PostgreSQL version (`16`)** | README badge, `docker-compose.yml`, `DEPLOYMENT.md` Cloud SQL create command, CI workflow service image |
+| **GCP region (`europe-southwest1`)** | Project Status table, DEPLOYMENT.md (every `gcloud` example) |
+| **Owner/repo (`starseeker-code/five-a-day`)** | README badges (CI, Codecov), GITHUB.md examples |
+| **Env var inventory** | `settings.py` (`os.getenv(...)`), README Env Variables Reference table, `.env.example`, DEPLOYMENT.md Secret Manager list. Every name that appears in one must appear in every other (modulo internal-only vars that never ship to production). |
+
+If a change appears in one place, **check the others**. A version-bump commit that only updates the badge but not Version History is a broken commit.
+
+---
+
+## Step 5 — Global stale-reference sweep
+
+Before reporting completion, grep the entire `docs/` tree, main README, all per-app READMEs, CLAUDE.md, and DEPLOYMENT.md for:
+
+- File names of any file deleted this session (e.g. `render.yaml`, old workflow files)
+- Old command names (if renamed)
+- Old env var names (if renamed)
+- Legacy service names (e.g. "Render", "Heroku") unless they are historical in Version History
+- Deprecated URL patterns
+- Removed models
+
+Commands:
+
+```bash
+grep -rn "" README.md CLAUDE.md DEPLOYMENT.md docs/ project/*/README.md project/ENROLLMENT_PAYMENT_SYSTEM.md
+```
+
+Any hit outside `.venv/` → fix.
+
+---
+
+## Step 6 — Report back
+
+After saving all docs, report in under 25 lines:
+
+- **Files updated** — list each file with a one-sentence summary of what changed
+- **Versions moved** — which versions entered/exited the Recent Versions table
+- **Inconsistencies resolved** — e.g. "Main README badge said 1.0.4, pyproject said 1.0.5 — aligned to 1.0.5"
+- **Cross-file syncs** — e.g. "Updated test count in main README, core/README.md, and CLAUDE.md"
+- **Stale references removed** — e.g. "Removed 4 remaining `render.yaml` mentions in Security and ToC"
+- **Anything you noticed but did NOT change** — sections that look stale but weren't in scope (surface these for the user to decide)
+- **Files NOT touched and why** — if only `docs/CELERY.md` was in scope for the staged diff, say so explicitly
+
+---
+
+## Guarantees
+
+- **Never commit.** The user may want to amend, combine, or review before committing.
+- **Never stage files** yourself without user confirmation.
+- **Never read `.env*` files** — they contain secrets.
+- **Never invent work** — if the staged diff is purely a bug fix with no documentation implications, say "no doc changes needed" and stop.
+- **Never fabricate counts** — run the grep/wc command to get the real test count, view count, etc.
+- **Flag version mismatches** between `pyproject.toml`, `settings.py`, and the most recent commit message — the user likely forgot `make version`.
+- **Always grep** for stale references before declaring victory. The routing table misses things; grep catches them.
+
+## When the staged diff is empty
+
+If `git diff --cached` is empty, either:
+1. The user forgot to stage anything → ask
+2. They want you to review the working tree instead → ask for confirmation and then `git diff` (unstaged) is the source of truth
+
+Do not guess which case it is.
+
+## When the staged diff spans multiple versions
+
+If the staged changes represent more than one logical version (e.g. a CI/CD refactor + a new feature + a bug fix), flag this to the user: they probably want to split the commit. Do not try to document them as a single version.
diff --git a/.env.testing.example b/.env.testing.example
deleted file mode 100644
index 882e1bc..0000000
--- a/.env.testing.example
+++ /dev/null
@@ -1,92 +0,0 @@
-# ============================================================================
-# FIVE A DAY — TESTING / QA ENVIRONMENT
-# ============================================================================
-# Copy this file to .env.testing and fill in the secrets:
-# cp .env.testing.example .env.testing
-#
-# Then start the QA environment:
-# make testing-up
-# make testing-seed
-
-# ============================================================================
-# DJANGO SETTINGS
-# ============================================================================
-VERSION=1.0.1t
-DJANGO_ENV=testing
-DJANGO_DEBUG=False
-DJANGO_SECRET_KEY='CHANGE_ME_generate_with_python_get_random_secret_key'
-DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0,.run.app
-
-# ============================================================================
-# HTTPS & SECURITY
-# ============================================================================
-SECURE_SSL_REDIRECT=False
-SESSION_COOKIE_SECURE=True
-CSRF_COOKIE_SECURE=True
-SESSION_COOKIE_SAMESITE=Strict
-CSRF_COOKIE_HTTPONLY=True
-CSRF_COOKIE_SAMESITE=Strict
-# CSRF_TRUSTED_ORIGINS=https://your-testing-domain.run.app
-
-# ============================================================================
-# LOGGING
-# ============================================================================
-LOG_LEVEL=INFO
-DJANGO_LOG_LEVEL=INFO
-
-# ============================================================================
-# DATABASE
-# ============================================================================
-DATABASE=postgres
-POSTGRES_DB=fiveaday_testing
-POSTGRES_USER=fiveaday_tester
-POSTGRES_PASSWORD='CHANGE_ME'
-POSTGRES_HOST=db
-POSTGRES_PORT=5432
-
-# ============================================================================
-# QA LOGIN CREDENTIALS
-# ============================================================================
-LOGIN_USERNAME=manitas
-LOGIN_PASSWORD=CHANGE_ME
-
-# ============================================================================
-# QA TESTING TOOLS ACCESS
-# ============================================================================
-# Only this username can see and use the /testing/ dashboard.
-QA_TESTING_USERNAME=manitas
-
-# ============================================================================
-# SUPERUSER (Django Admin)
-# ============================================================================
-DJANGO_SUPERUSER_USERNAME=qa_admin
-DJANGO_SUPERUSER_EMAIL=qa@fiveaday.example.com
-DJANGO_SUPERUSER_PASSWORD='CHANGE_ME'
-
-# ============================================================================
-# EMAIL
-# ============================================================================
-EMAIL_HOST_USER=your-email@gmail.com
-EMAIL_SECRET='your-gmail-app-password'
-SUPPORT_EMAIL=your-support@email.com
-
-# ============================================================================
-# CELERY / REDIS (not available — tasks run synchronously)
-# ============================================================================
-#CELERY_BROKER_URL=redis://redis:6379/0
-#CELERY_RESULT_BACKEND=redis://redis:6379/0
-
-# ============================================================================
-# GOOGLE OAUTH (optional)
-# ============================================================================
-#GOOGLE_CLIENT_ID=
-#GOOGLE_CLIENT_SECRET=
-#GOOGLE_REDIRECT_URI=
-
-# ============================================================================
-# ACADEMY BUSINESS INFO
-# ============================================================================
-ACADEMY_IBAN=ES00 0000 0000 0000 0000 0000
-ACADEMY_IBAN_HOLDER=Nombre del titular
-ACADEMY_PHONE=600 000 000
-ACADEMY_WHATSAPP=600000000
diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml
index e68195b..64d9453 100644
--- a/.github/workflows/auto-merge.yml
+++ b/.github/workflows/auto-merge.yml
@@ -54,12 +54,54 @@ jobs:
exit 0
fi
+ # 3. Has the version been bumped on development?
+ # Source of truth is pyproject.toml — it's what `make version` and `make pc-run` write.
+ # A version bump is REQUIRED: no version change → no merge, even with 24 h of new commits.
+ extract_version() {
+ # $1 = ref (e.g. origin/testing or origin/development)
+ git show "$1:pyproject.toml" 2>/dev/null \
+ | grep -E '^[[:space:]]*version[[:space:]]*=' \
+ | head -1 \
+ | sed -E 's/.*version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/'
+ }
+
+ TESTING_VERSION=$(extract_version origin/testing)
+ DEV_VERSION=$(extract_version origin/development)
+
+ if [ -z "$TESTING_VERSION" ] || [ -z "$DEV_VERSION" ]; then
+ echo "Could not read version from pyproject.toml on one of the branches (testing='$TESTING_VERSION', development='$DEV_VERSION') — skipping."
+ echo "should_merge=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ echo "testing version: $TESTING_VERSION"
+ echo "development version: $DEV_VERSION"
+
+ if [ "$DEV_VERSION" = "$TESTING_VERSION" ]; then
+ echo "Version unchanged ($DEV_VERSION) — development has new commits but no version bump. Skipping merge."
+ echo "Run 'make pc-run' (answer yes) or 'make version x.y.z' on development before the next cron tick to unlock the merge."
+ echo "should_merge=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ # Semver sort (GNU sort -V handles both "1.0.5" and "v1.0.5"). The highest value MUST be DEV_VERSION.
+ HIGHEST=$(printf '%s\n%s\n' "$TESTING_VERSION" "$DEV_VERSION" | sort -V | tail -1)
+ if [ "$HIGHEST" != "$DEV_VERSION" ]; then
+ echo "development version ($DEV_VERSION) is LOWER than testing ($TESTING_VERSION) — this should not happen. Refusing to merge backwards."
+ echo "should_merge=false" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ echo "Version bump confirmed: $TESTING_VERSION → $DEV_VERSION"
+
LAST_SHA=$(git rev-parse origin/development)
LAST_MSG=$(git log origin/development -1 --format="%s")
- echo "should_merge=true" >> "$GITHUB_OUTPUT"
- echo "last_sha=$LAST_SHA" >> "$GITHUB_OUTPUT"
- echo "last_msg=$LAST_MSG" >> "$GITHUB_OUTPUT"
+ echo "should_merge=true" >> "$GITHUB_OUTPUT"
+ echo "last_sha=$LAST_SHA" >> "$GITHUB_OUTPUT"
+ echo "last_msg=$LAST_MSG" >> "$GITHUB_OUTPUT"
+ echo "new_version=$DEV_VERSION" >> "$GITHUB_OUTPUT"
+ echo "old_version=$TESTING_VERSION" >> "$GITHUB_OUTPUT"
- name: Check CI status on development
id: ci
@@ -124,6 +166,30 @@ jobs:
echo "merge_msg=$MERGE_MSG" >> "$GITHUB_OUTPUT"
echo "Merged with message: $MERGE_MSG"
+ - name: Tag the testing merge commit with the staging tag
+ id: tag
+ if: steps.merge.conclusion == 'success'
+ run: |
+ NEW_VER="${{ steps.conditions.outputs.new_version }}"
+ TAG="testing-v${NEW_VER}"
+
+ # Defensive — the version-progression check should prevent collisions,
+ # but if the tag exists already, skip rather than clobber.
+ if git ls-remote --tags origin "refs/tags/$TAG" | grep -q "refs/tags/$TAG"; then
+ echo "Tag $TAG already exists on origin — skipping tag creation."
+ echo "created=false" >> "$GITHUB_OUTPUT"
+ echo "tag=$TAG" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ # Annotated tag — richer metadata than a lightweight tag, shows up in the Tags tab.
+ git tag -a "$TAG" -m "Staging $TAG — auto-merged development → testing. The matching release tag v${NEW_VER} will be created on main when the PR lands."
+ git push origin "refs/tags/$TAG"
+
+ echo "Created and pushed staging tag $TAG on testing HEAD."
+ echo "created=true" >> "$GITHUB_OUTPUT"
+ echo "tag=$TAG" >> "$GITHUB_OUTPUT"
+
- name: Create PR testing → main
id: pr
if: steps.merge.conclusion == 'success'
@@ -157,6 +223,12 @@ jobs:
'',
'This PR was created automatically after `development` was merged into `testing`.',
'',
+ `**Version:** \`${{ steps.conditions.outputs.old_version }}\` → \`${{ steps.conditions.outputs.new_version }}\``,
+ `**Staging tag (on testing):** \`${{ steps.tag.outputs.tag }}\``,
+ `**Release tag (on main, created after this PR merges):** \`v${{ steps.conditions.outputs.new_version }}\``,
+ '',
+ 'Merge strategy is flexible — staging and release tags are independent, so squash/rebase/merge-commit all work.',
+ '',
'### Pre-merge checklist',
'- [ ] All CI checks passing on this PR',
'- [ ] Manual review completed',
@@ -189,6 +261,9 @@ jobs:
A new merge has landed on the testing branch and a pull request to main is now awaiting review.
+ | Version bump | ${{ steps.conditions.outputs.old_version }} → ${{ steps.conditions.outputs.new_version }} |
+ | Staging tag (on testing) | ${{ steps.tag.outputs.tag }} (${{ steps.tag.outputs.created == 'true' && 'newly created' || 'pre-existing' }}) |
+ | Release tag (pending) | v${{ steps.conditions.outputs.new_version }} — will be created on main after the PR merges |
| Merge commit | ${{ steps.merge.outputs.merge_msg }} |
| Repository | ${{ github.repository }} |
| PR status | ${{ steps.pr.outputs.already_existed == 'true' && 'Updated (PR already existed)' || 'Newly created' }} |
diff --git a/.github/workflows/notify-production.yml b/.github/workflows/notify-production.yml
index 8a687e9..47a9a7e 100644
--- a/.github/workflows/notify-production.yml
+++ b/.github/workflows/notify-production.yml
@@ -8,7 +8,7 @@ on:
branches: [main]
permissions:
- contents: read
+ contents: write # needs write for pushing the release tag
jobs:
notify:
@@ -19,6 +19,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Need HEAD and HEAD~1 for diff summary
+ token: ${{ secrets.GH_PAT || github.token }}
- name: Gather commit info
id: info
@@ -36,6 +37,42 @@ jobs:
echo "files_changed=$FILES_CHANGED"
} >> "$GITHUB_OUTPUT"
+ - name: Create release tag on main
+ id: tag
+ run: |
+ # Read the authoritative version from pyproject.toml on the just-pushed commit
+ VERSION=$(grep -E '^[[:space:]]*version[[:space:]]*=' pyproject.toml \
+ | head -1 \
+ | sed -E 's/.*version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/')
+
+ if [ -z "$VERSION" ]; then
+ echo "Could not read version from pyproject.toml — skipping release tag."
+ echo "created=false" >> "$GITHUB_OUTPUT"
+ echo "tag=(skipped)" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ TAG="v${VERSION}"
+
+ # Skip if the release tag already exists (e.g. a main push that doesn't bump version,
+ # like a direct docs fix or a re-deploy of the same version).
+ if git ls-remote --tags origin "refs/tags/$TAG" | grep -q "refs/tags/$TAG"; then
+ echo "Release tag $TAG already exists — skipping."
+ echo "created=false" >> "$GITHUB_OUTPUT"
+ echo "tag=$TAG" >> "$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ git tag -a "$TAG" -m "Release $TAG (production) — merged from testing"
+ git push origin "refs/tags/$TAG"
+
+ echo "Created and pushed release tag $TAG on main HEAD."
+ echo "created=true" >> "$GITHUB_OUTPUT"
+ echo "tag=$TAG" >> "$GITHUB_OUTPUT"
+
- name: Send email
uses: dawidd6/action-send-mail@v3
with:
@@ -52,6 +89,7 @@ jobs:
A new commit has landed on the main branch. Production deploy is ready when you are.
+ | Release tag | ${{ steps.tag.outputs.tag }} (${{ steps.tag.outputs.created == 'true' && 'newly created' || 'already existed — no version bump on this push' }}) |
| Commit | ${{ steps.info.outputs.message }} |
| Author | ${{ steps.info.outputs.author }} |
| SHA | ${{ steps.info.outputs.short_sha }} |
diff --git a/.gitignore b/.gitignore
index 4f24347..5842120 100644
--- a/.gitignore
+++ b/.gitignore
@@ -212,13 +212,10 @@ __marimo__/
# ============================================================================
# DOCKER & ENVIRONMENT
# ============================================================================
-# Variables de entorno (¡NUNCA subir .env con contraseñas reales!)
+# Variables de entorno (¡NUNCA subir ningún .env!)
+# No hay excepciones: la plantilla de .env vive en README.md (sección ".env template")
.env*
-# Mantener el ejemplo
-!.env.example
-!.env.testing.example
-
# Volúmenes de Docker
postgres_data/
redis_data/
diff --git a/CLAUDE.md b/CLAUDE.md
index 93ebee3..8114f8a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -53,8 +53,8 @@ make test # Run tests (PostgreSQL)
```bash
make lint # Ruff linter
make format # Ruff formatter
-make pre-commit-run # Run all pre-commit hooks
-make test # Run 294 tests (PostgreSQL via Docker, parallel, 70% coverage)
+make pc-run # Run all pre-commit hooks (dry run + auto version bump)
+make test # Run 283 tests (PostgreSQL via Docker, parallel, with coverage)
```
- **UV** for dependency management (see [docs/UV.md](docs/UV.md))
@@ -126,7 +126,25 @@ All pricing flows through `billing/services/`. The single source of truth is `Si
- **Celery eager mode** — without Redis, tasks run synchronously. Don't rely on task.delay() being truly async in development.
- **`#webcrumbs` removed** — the old CSS scoping wrapper is gone. If you see references to it in old code, delete them.
- **Template names are English** — all email templates were renamed from Spanish (e.g., `matricula_niño.html` → `enrollment_child.html`). Never create templates with Spanish names.
-- **Version in two places** — `pyproject.toml` and `settings.py`. Use `make version V=x.y.z` to update both.
+- **Version in two places** — `pyproject.toml` and `project/project/settings.py`. Use `make version x.y.z` (positional, with y/N confirmation) to update both.
+- **APP_VERSION in `.env`** — the local `.env` may contain a legacy `APP_VERSION=0.x.y` line. Either remove the line or update it — it silently overrides the default in `settings.py` at runtime.
+- **`pc-run` renamed** — the old `make pre-commit-run` target is now `make pc-run`. It also auto-bumps the patch version on a clean pass (y/N prompt) and auto-stages `uv.lock` if regenerated.
+
+## README maintenance (MUST do at end of every work session)
+
+The `README.md` must stay in sync with the code. At the end of any non-trivial change, verify and update these sections before handing off:
+
+1. **Header badges** — version badge must match `pyproject.toml`
+2. **Project Status table** — three rows in order Production → Testing → Development, each with branch + hosting + CI badge
+3. **Recent Versions table** — keep only the **last 3** versions. Entries must include: version, date (YYYY-MM-DD), a dense description mentioning every user-visible change in that version. When a new patch ships, drop the oldest row.
+4. **Version History `` blocks** — add a new `` block for the new version; remove the `open` attribute from the previous one. Structure: `**Subsection**` headings + bullet lists. Pull subjects from `git log` for the commits in that version.
+5. **Directory Layout** — if directories, tool counts, test counts, or Make command counts changed, update them here. `tests/` line must show current test count and coverage percentage.
+6. **Make Commands table** — every renamed or new `make` target must appear or be updated.
+7. **Contributing → Development Workflow** — if the developer flow changed (new pre-commit behavior, new commands), update the numbered list.
+8. **Table of Contents** — every new section or renamed heading must have a matching ToC entry with a valid anchor (GitHub generates anchors by lowercasing, replacing spaces with `-`, dropping non-alphanumerics except `-`).
+9. **Delete stale content** — if a file or service was removed (e.g. `render.yaml`, a retired workflow), remove every reference to it. Grep the README for the name first.
+
+**When the user invokes the `update-readme` skill**, use the staged changes (`git diff --cached`, `git status --porcelain`, `git diff --cached --stat`) to determine what changed, then apply the checklist above. Do not guess — inspect the staged diff first.
## Django Best Practices (enforced in this project)
diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
index a30c8c0..607676b 100644
--- a/DEPLOYMENT.md
+++ b/DEPLOYMENT.md
@@ -96,7 +96,8 @@ Verify with `free -h` — you should see ~2 GB in the Swap row.
```bash
git clone https://github.com/YOUR_ORG/five-a-day.git
cd five-a-day
-cp .env.example .env.testing # Edit with testing values
+# Create .env.testing and populate it using the template in README.md (section ".env template")
+touch .env.testing
docker compose --env-file .env.testing up -d
```
diff --git a/Makefile b/Makefile
index d1566f5..e9581bf 100644
--- a/Makefile
+++ b/Makefile
@@ -23,7 +23,7 @@ help:
@echo " =========================="
@echo ""
@echo " Setup & Build:"
- @echo " make setup Copy .env.example to .env"
+ @echo " make setup Create empty .env (fill in from README.md '.env template')"
@echo " make build Build Docker images"
@echo " make rebuild Full rebuild (no cache) + start"
@echo " make rebuild-web Rebuild only the web image"
@@ -120,8 +120,8 @@ help:
# ============================================================================
setup:
@if [ ! -f .env ]; then \
- cp .env.example .env; \
- echo "Created .env — edit it with your configuration."; \
+ touch .env; \
+ echo "Created empty .env. Copy the template from README.md (section '.env template') into .env and fill in the blanks."; \
else \
echo ".env already exists."; \
fi
@@ -488,6 +488,10 @@ pc-run:
echo "Updated version $$CURRENT with new version $$NEW"; \
fi; \
fi
+ @if [ -n "$$(git status --porcelain uv.lock 2>/dev/null)" ]; then \
+ git add uv.lock; \
+ echo "Staged updated uv.lock — next git commit will not be blocked by it"; \
+ fi
# ============================================================================
# PRODUCTION
diff --git a/readme.md b/README.md
similarity index 81%
rename from readme.md
rename to README.md
index 0856185..dd398de 100644
--- a/readme.md
+++ b/README.md
@@ -9,11 +9,12 @@
-
+
-
+
+
---
@@ -29,26 +30,19 @@ Built to centralize student records, automate billing cycles, and streamline par
### Project Status
-| Environment | Version | Status |
-|-------------|---------|--------|
-| **Production** | v1.0.4 |  |
-| **Testing (QA)** | v1.0.4 |  |
-| **Development** | v1.0.4 |  |
+Live status for each environment is pulled from GitHub Actions — the badges below reflect the real state of CI on each branch.
-| | |
-|---|---|
-| **Documentation** | This README, [DEPLOYMENT.md](DEPLOYMENT.md), [GITHUB.md](docs/GITHUB.md), [HTTPS.md](docs/HTTPS.md), [UV.md](docs/UV.md), [CELERY.md](docs/CELERY.md), per-app READMEs, [CLAUDE.md](CLAUDE.md) |
+| Environment | Branch | Hosting | CI Status |
+|-------------|--------|---------|-----------|
+| **Production** | `main` | GCP Cloud Run + Cloud SQL (PostgreSQL 16), `europe-southwest1` | [](https://github.com/starseeker-code/five-a-day/actions/workflows/ci.yml?query=branch%3Amain) |
+| **Testing (QA)** | `testing` | GCP Compute Engine e2-micro (free tier), Docker Compose | [](https://github.com/starseeker-code/five-a-day/actions/workflows/ci.yml?query=branch%3Atesting) |
+| **Development** | `development` | Local machine via `make up` (Docker Compose) | [](https://github.com/starseeker-code/five-a-day/actions/workflows/ci.yml?query=branch%3Adevelopment) |
| Version | Date | Description |
|---------|------|-------------|
-| **v1.0.4** | 2026-04-15 | GitHub Actions CI/CD pipeline (lint, typecheck, tests, CodeQL, Dependabot), auto-merge `development` → `testing` with 24h delay + auto-PR to `main`, email notifications, branch protection rules, inspirational quote generator, GCP deployment plan, cleaned legacy Render config, `make version x.y.z` + `make pc-run` with auto version bump |
-| v1.0.3 | 2026-04-14 | Test coverage raised to 70% — 294 tests total (40+ new tests) across auth views, comms services, dashboard, schedule, management, and apps modules |
-| v1.0.2 | 2026-04-13 | Replaced Poetry with UV, full developer tooling (Ruff, mypy, bandit, pip-audit, pre-commit, pytest-cov, pytest-xdist, pytest-randomly) |
-| v1.0.1t | 2026-04-14 | QA/testing environment: `/testing/` dashboard, database seeding, backlog with email, error reporting, HTTPS guide, access control via `QA_TESTING_USERNAME` |
-| v1.0.0 | 2026-04-11 | Security hardening, query optimization (Case/When aggregates, N+1 fixes), GCP config, transaction safety |
-| v1.0.0 | 2026-04-10 | Multi-app architecture, service layer, 132 tests, frontend cleanup, full documentation |
-| v0.30.2 | 2025-03-14 | History system, GDPR for adults, Docker Compose workflow |
-| v0.29.0 | 2025-03-01 | Enrollment system with discounts, adult students, email automation |
+| **v1.0.5** | 2026-04-15 | GitHub Actions CI/CD pipeline (lint, typecheck, tests, CodeQL, Dependabot), auto-merge `development` → `testing` with 24 h delay + auto-PR to `main`, email notifications, branch protection rules for public repo hardening, `make pc-run` auto-stages regenerated `uv.lock` |
+| v1.0.4 | 2026-04-15 | Inspirational quote generator on `/home` (48 h cookie rotation), GCP deployment plan ([DEPLOYMENT.md](DEPLOYMENT.md)), Celery worker + beat containers, cleaned legacy Render config, `make version x.y.z` positional arg with confirmation guard, `make pc-run` (renamed from `pre-commit-run`) with auto version bump |
+| v1.0.3 | 2026-04-14 | Test coverage raised to **70 %** — 13 new test files across auth views, comms services, app forms, constants, create payment views, exports, forms, parent views, payment views, schedule views, student forms, student views, transactions |
---
@@ -81,6 +75,7 @@ Built to centralize student records, automate billing cycles, and streamline par
- [Key Constraints](#key-constraints)
- [Development \& Docker](#development--docker)
- [Quick Start](#quick-start)
+ - [.env template](#env-template)
- [Make Commands](#make-commands)
- [Environment Configuration](#environment-configuration)
- [Environment Variables Reference](#environment-variables-reference)
@@ -120,8 +115,7 @@ Built to centralize student records, automate billing cycles, and streamline par
- [Security Headers](#security-headers)
- [Infrastructure \& Deployment](#infrastructure--deployment-1)
- [Docker](#docker)
- - [Render (render.yaml)](#render-renderyaml)
- - [Google Cloud Run (gcp-cloudrun.yaml)](#google-cloud-run-gcp-cloudrunyaml)
+ - [Google Cloud Run](#google-cloud-run)
- [Secrets Management](#secrets-management)
- [Email Security](#email-security)
- [Data Protection \& Input Validation](#data-protection--input-validation)
@@ -135,7 +129,6 @@ Built to centralize student records, automate billing cycles, and streamline par
- [Error pages you might see](#error-pages-you-might-see)
- [For developers: how the QA environment works](#for-developers-how-the-qa-environment-works)
- [Access control for /testing/](#access-control-for-testing)
- - [GCP deployment plan](#gcp-deployment-plan)
- [CI/CD \& GitHub Actions](#cicd--github-actions)
- [Pipeline Overview](#pipeline-overview)
- [Branch Strategy](#branch-strategy)
@@ -158,39 +151,55 @@ Built to centralize student records, automate billing cycles, and streamline par
## Version History & Roadmap
-
-v1.0.4 — CI/CD Pipeline, GCP Migration Plan, Quote Generator (current)
+
+v1.0.5 — CI/CD Pipeline & Public Repo Hardening (current)
**GitHub Actions CI/CD** (new — see [docs/GITHUB.md](docs/GITHUB.md))
-- `ci.yml` — three parallel jobs on every push/PR: Ruff + Bandit lint, mypy type check, pytest against PostgreSQL 16 service container with coverage uploaded to Codecov
-- `auto-merge.yml` — hourly cron that merges `development` → `testing` after 24h of inactivity and CI passing, then auto-creates a PR `testing` → `main`
+- `ci.yml` — three parallel jobs on every push/PR: Ruff + Bandit lint, mypy type check, pytest against a PostgreSQL 16 service container with coverage uploaded to Codecov
+- `auto-merge.yml` — hourly cron that merges `development` → `testing` after 24 h of inactivity and CI passing, then auto-creates a PR `testing` → `main`
- `codeql.yml` — weekly Python security analysis (OWASP Top 10, Django-specific queries)
-- `notify-production.yml` — emails `hellofiveaday@gmail.com` on every push to `main` with commit info and deploy instructions
-- Owner email notifications when `development` → `testing` merge lands + PR opened to `main`
+- `notify-production.yml` — emails `hellofiveaday@gmail.com` on every push to `main` with commit info and `gcloud` deploy instructions
+- Owner email notifications when `development` → `testing` merge lands and a PR is opened to `main`
- `dependabot.yml` — grouped weekly Python and GitHub Actions updates targeting `development`
- `CODEOWNERS` — auto-request reviews from both owner accounts
+**Public-repo hardening**
+
+- Branch protection rules documented for `main` (14 protections) and `testing` (minimal)
+- Secret scanning + push protection + CodeQL enabled (all free for public repos)
+- Fork PR workflow restriction, read-only default workflow permissions, block-approvals-from-Actions
+- `SECURITY.md` + `CODEOWNERS` + `LICENSE` required-file checklist in [docs/GITHUB.md](docs/GITHUB.md)
+
+**Developer tooling**
+
+- `make pc-run` auto-stages regenerated `uv.lock` as the final step — next `git commit` is no longer blocked by the lock file
+
+
+
+
+v1.0.4 — GCP Migration Plan, Quote Generator, Celery
+
**GCP migration plan** (new — see [DEPLOYMENT.md](DEPLOYMENT.md))
- Full Cloud Run + Cloud SQL architecture documented
- Three environments: local Docker (dev), Compute Engine e2-micro free tier (testing), Cloud Run + Cloud SQL (production)
- Cost estimate: ~$15-27/month for production, $0/month for testing
- Celery replacement strategy using Cloud Scheduler + Cloud Run Jobs
-- Cleaned legacy Render config (`render.yaml` removed, commented nginx and pgAdmin services removed from docker-compose)
+- Cleaned legacy Render config — `render.yaml` removed; commented nginx and pgAdmin services removed from `docker-compose.yml`
**Dashboard enhancement**
-- Inspirational quote generator on `/home` — fetches two daily quotes from `zenquotes.io`, stores them in a 48h cookie, rotates daily (day 0 shows quote 1, day 1+ shows quote 2), graceful fallback to the default Spanish subtitle on API failure
+- Inspirational quote generator on `/home` — fetches two daily quotes from `zenquotes.io`, stores them in a 48 h cookie, rotates daily (day 0 shows quote 1, day 1+ shows quote 2), graceful fallback to the default Spanish subtitle on API failure
**Developer tooling**
- `make version x.y.z` — positional argument (replaces `V=x.y.z`) with confirmation guard before writing
- `make pc-run` — renamed from `pre-commit-run`; after a clean pass, prompts to auto-increment the patch version in `pyproject.toml` and `project/settings.py`
-**Bug fixes**
+**Celery**
-- Celery worker and beat containers added to docker-compose with correct permissions and health checks
+- Celery worker and beat containers added to `docker-compose.yml` with correct permissions and health checks
- Several payment and enrollment issues fixed
@@ -200,15 +209,15 @@ Built to centralize student records, automate billing cycles, and streamline par
**Testing**
-- Test count raised from 252 to **294** (40+ new tests)
+- 40+ new tests added across 13 new test files — overall suite around 280+ tests
- Coverage raised to **70%** across `core`, `students`, `billing`, `comms`
-- New test files: `test_auth_views.py`, `test_comms_services.py`, `test_dashboard.py`, `test_schedule_views.py`, `test_management_views.py`, `test_app_forms.py`
+- New test files: `test_auth_views.py`, `test_app_form_views.py`, `test_constants.py`, `test_create_payment_views.py`, `test_exports.py`, `test_forms.py`, `test_parent_views.py`, `test_payment_views.py`, `test_schedule_views.py`, `test_student_forms.py`, `test_student_views.py`, `test_transactions.py`
- Additional parametrized test cases for email-form views and error pages
**Coverage tooling**
-- `coverage.svg` badge now reflects the improved coverage
-- `make coverage-badge` command streamlines badge regeneration
+- Coverage badge pulled dynamically from Codecov (CI workflow uploads `coverage.xml` on every run)
+- `make coverage-badge` retained for offline SVG generation
@@ -628,15 +637,17 @@ erDiagram
git clone https://github.com/starseeker-code/five-a-day.git
cd five-a-day
-# Configure environment
-cp .env.example .env # Edit with your values (see Environment Configuration below)
+# Create the .env file — copy the template below into `.env` and fill in the blanks
+touch .env
```
+Paste the template from [.env template](#env-template) into your new `.env` file and fill in the empty values.
+
**Docker (recommended):**
```bash
make build # Build images
-make up # Start PostgreSQL + Django → http://localhost:8000
+make up # Start PostgreSQL + Redis + Django + Celery → http://localhost:8000
make migrate # Apply migrations (first time only)
```
@@ -653,6 +664,87 @@ python manage.py runserver
> - `DJANGO_ENV=development` — enables development behaviors (auto superuser, no collectstatic)
> - `DJANGO_DEBUG=true` — enables Django debug mode, detailed error pages
> - `POSTGRES_PASSWORD` — required for database connection
+> - `DJANGO_SECRET_KEY` — generate with `python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'`
+
+### .env template
+
+`.env` is gitignored and never committed. The template below is the authoritative structure — copy it into your new `.env` file, then fill in the empty values with your own secrets. Defaults that are safe to keep as-is are already filled in.
+
+```bash
+# ============================================================================
+# DJANGO SETTINGS
+# ============================================================================
+DJANGO_ENV=development # production | development
+DJANGO_DEBUG=True
+SECURE_SSL_REDIRECT=False
+# Generate with: python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'
+DJANGO_SECRET_KEY=
+DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
+
+# ============================================================================
+# LOGGING
+# ============================================================================
+LOG_LEVEL=INFO
+DJANGO_LOG_LEVEL=INFO
+
+# ============================================================================
+# DATABASE CONFIGURATION
+# ============================================================================
+DATABASE=postgres # sqlite | postgres
+POSTGRES_DB=fiveaday_db
+POSTGRES_USER=fiveaday_user
+# Generate with: openssl rand -base64 32
+POSTGRES_PASSWORD=
+POSTGRES_HOST=db # `db` in Docker, `localhost` outside
+POSTGRES_PORT=5432
+
+# ============================================================================
+# SUPERUSER (auto-created on first boot if all three are set)
+# ============================================================================
+DJANGO_SUPERUSER_USERNAME=
+DJANGO_SUPERUSER_EMAIL=
+DJANGO_SUPERUSER_PASSWORD=
+
+# ============================================================================
+# EMAIL CONFIGURATION (Gmail SMTP + App Password)
+# ============================================================================
+EMAIL_HOST_USER= # your-academy@gmail.com
+EMAIL_SECRET= # 16-char Gmail App Password
+SUPPORT_EMAIL= # where support tickets are sent
+EMAIL_TEST_1= # dev test recipient 1
+EMAIL_TEST_2= # dev test recipient 2
+
+# ============================================================================
+# CELERY / REDIS
+# ============================================================================
+CELERY_BROKER_URL=redis://redis:6379/0
+CELERY_RESULT_BACKEND=redis://redis:6379/0
+
+# ============================================================================
+# AUTHENTICATION (session-based, until the Django User model is adopted in v1.6)
+# ============================================================================
+LOGIN_USERNAME=fiveaday
+LOGIN_PASSWORD=
+
+# ============================================================================
+# GOOGLE OAUTH
+# ============================================================================
+# Create at https://console.cloud.google.com/ → APIs & Services → Credentials
+# Authorised redirect URI: http://localhost:8000/auth/google/callback/
+GOOGLE_CLIENT_ID=
+GOOGLE_CLIENT_SECRET=
+GOOGLE_REDIRECT_URI=http://localhost:8000/auth/google/callback/
+
+# ============================================================================
+# ACADEMY BUSINESS INFO (prefilled in payment-reminder email forms)
+# ============================================================================
+ACADEMY_IBAN=
+ACADEMY_IBAN_HOLDER=
+ACADEMY_PHONE=
+ACADEMY_WHATSAPP=
+```
+
+**Note**: do not include `VERSION=` in your `.env` — it is deprecated. The app version is derived from `pyproject.toml` (and overridable via `APP_VERSION`).
### Make Commands
@@ -685,8 +777,16 @@ Run `make` or `make help` for the full list. Key commands:
| `make test-fast` | Stop on first failure |
| `make test-k K=payment` | Run tests matching keyword |
| **Versioning** | |
-| `make version V=1.1.0` | Update version in pyproject.toml + settings.py |
-| `make version` | Show current version locations |
+| `make version 1.1.0` | Update version in `pyproject.toml` + `settings.py` (with y/N confirmation) |
+| `make version` | Show current version |
+| **Developer Tooling** | |
+| `make lint` / `make lint-fix` | Run Ruff linter (optionally auto-fix) |
+| `make format` / `make format-check` | Run Ruff formatter |
+| `make mypy` | Run mypy type checker |
+| `make bandit` | Run bandit security linter |
+| `make audit` | `pip-audit` — scan deps for CVEs |
+| `make pc-run` | Run pre-commit on all files; on clean pass, offer to auto-bump patch version; auto-stages regenerated `uv.lock` |
+| `make pre-commit-install` | Install the git pre-commit hook |
| **Email & Payments** | |
| `make send-test-email` | Send test birthday email |
| `make test-all-emails` | List all email templates |
@@ -712,13 +812,16 @@ The database is **always PostgreSQL** — in Docker development, in tests, and i
### Environment Variables Reference
+The table below describes every variable in the [.env template](#env-template) above, plus a few advanced overrides not included in the template. See the template for the full `.env` structure.
+
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
-| **Core** | | | |
-| `DJANGO_ENV` | Environment: `development` / `production` | No | `development` |
+| **Django core** | | | |
+| `DJANGO_ENV` | Environment: `development` / `production` / `testing` | No | `development` |
| `DJANGO_DEBUG` | Debug mode: `true` / `false` | No | `false` |
| `DJANGO_SECRET_KEY` | Secret key | **Yes in production** | dev fallback |
| `DJANGO_ALLOWED_HOSTS` | Comma-separated hosts | No | `localhost,127.0.0.1` |
+| `SECURE_SSL_REDIRECT` | Force HTTPS redirects | No | `True` when `DEBUG=False` |
| **Database** | | | |
| `DATABASE` | Set to `postgres` for PostgreSQL | No | `postgres` |
| `DATABASE_URL` | Full URL (Cloud deployments) | No | — |
@@ -727,23 +830,36 @@ The database is **always PostgreSQL** — in Docker development, in tests, and i
| `POSTGRES_PASSWORD` | Database password | **Yes** | — |
| `POSTGRES_HOST` | Database host | No | `db` (Docker) |
| `POSTGRES_PORT` | Database port | No | `5432` |
+| **Superuser** (auto-created on first boot when all three are set) | | | |
+| `DJANGO_SUPERUSER_USERNAME` | Superuser name | No | — |
+| `DJANGO_SUPERUSER_EMAIL` | Superuser email | No | — |
+| `DJANGO_SUPERUSER_PASSWORD` | Superuser password | No | — |
| **Email** | | | |
| `EMAIL_HOST_USER` | Gmail address | For email features | — |
| `EMAIL_SECRET` | Gmail app password | For email features | — |
| `SUPPORT_EMAIL` | Support ticket recipient | No | — |
| `EMAIL_TEST_1` / `EMAIL_TEST_2` | Test email recipients | No | — |
| **Auth** | | | |
-| `LOGIN_USERNAME` | Admin username | No | `fiveaday` |
-| `LOGIN_PASSWORD` | Admin password | No | `Fiveaday123!` |
+| `LOGIN_USERNAME` | Admin username | **Yes** | — (login refused if missing) |
+| `LOGIN_PASSWORD` | Admin password | **Yes** | — (login refused if missing) |
+| `QA_TESTING_USERNAME` | Extra user allowed to see `/testing/` dashboard | No (QA only) | — |
| `GOOGLE_CLIENT_ID` | OAuth client ID | For Google login | — |
| `GOOGLE_CLIENT_SECRET` | OAuth client secret | For Google login | — |
| `GOOGLE_REDIRECT_URI` | OAuth callback URL | For Google login | auto-detected |
-| `GOOGLE_ALLOWED_EMAIL` | Restrict Google login | No | `EMAIL_HOST_USER` |
-| **Other** | | | |
-| `APP_VERSION` | Version string | No | from settings.py |
-| `CELERY_BROKER_URL` | Redis URL for Celery | No | eager mode |
-| `SESSION_COOKIE_AGE` | Session duration (seconds) | No | `86400` (24h) |
-| `LOG_LEVEL` | Logging level | No | `DEBUG`/`INFO` |
+| `GOOGLE_ALLOWED_EMAIL` | Restrict Google login to one email | No | `EMAIL_HOST_USER` |
+| **Celery / Redis** | | | |
+| `CELERY_BROKER_URL` | Redis URL for Celery | No | eager mode (tasks run inline) |
+| `CELERY_RESULT_BACKEND` | Redis URL for results | No | same as broker |
+| **Academy business info** (prefills payment-reminder email forms) | | | |
+| `ACADEMY_IBAN` | Bank account for payment reminders | No | — |
+| `ACADEMY_IBAN_HOLDER` | IBAN account holder | No | — |
+| `ACADEMY_PHONE` | Phone for Bizum payments | No | — |
+| `ACADEMY_WHATSAPP` | WhatsApp number for reminders | No | — |
+| **Logging / misc** | | | |
+| `LOG_LEVEL` | App log level | No | `DEBUG` in dev, `INFO` in prod |
+| `DJANGO_LOG_LEVEL` | Django framework log level | No | inherits `LOG_LEVEL` |
+| `APP_VERSION` | Version string override | No | from `settings.py` default |
+| `SESSION_COOKIE_AGE` | Session duration (seconds) | No | `86400` (24 h) |
### App Versioning
@@ -752,10 +868,12 @@ The app version is defined in **two places** and should be updated together:
1. **`pyproject.toml`** line 3: `version = "x.y.z"` — package metadata
2. **`project/settings.py`** line 17: `APP_VERSION = os.getenv("APP_VERSION", "x.y.z")` — runtime fallback
-Use `make version V=1.1.0` to update both at once. The version appears in:
+Use `make version x.y.z` (positional) to update both at once — it prompts `Version A will become the new version B, are you sure?` before writing. `make pc-run` also auto-bumps the patch digit on successful pre-commit if you answer `y` when asked.
+
+The version appears in:
- `/health/` endpoint response
- Support ticket emails
-- Can be overridden at runtime via the `APP_VERSION` environment variable
+- Can be overridden at runtime via the `APP_VERSION` environment variable (do **not** leave a legacy value like `0.x.y` in `.env` — remove the line so the default in `settings.py` takes effect)
---
@@ -809,9 +927,13 @@ five-a-day/
│ │
│ ├── core/ Dashboard, Auth, Schedule, Utilities
│ │ ├── models.py TodoItem, HistoryLog, FunFridayAttendance, ScheduleSlot
-│ │ ├── views/ 12 view modules
+│ │ ├── views/ 13 view modules (dashboard, auth, students, parents,
+│ │ │ payments, management, app_forms, schedule,
+│ │ │ fun_friday_attendance, todos, support, errors,
+│ │ │ testing_tools)
│ │ ├── constants.py DIAS_ES, MESES_ES, SCHEDULED_APPS
-│ │ ├── middleware.py SimpleAuthMiddleware
+│ │ ├── middleware.py SimpleAuthMiddleware, QAErrorEmailMiddleware
+│ │ ├── decorators.py qa_access_required (testing env gate)
│ │ ├── context_processors.py Notifications injected into all templates
│ │ ├── transactions.py Optimized queryset builders
│ │ ├── templates/ ALL HTML templates (base, pages, emails)
@@ -831,7 +953,7 @@ five-a-day/
│ │ ├── exports.py Excel/CSV builders
│ │ ├── admin.py Payment + Enrollment admin with actions
│ │ ├── urls.py 20 URL patterns
-│ │ └── management/commands/ generate_payments
+│ │ └── management/commands/ generate_payments, seed_testdata
│ │
│ ├── comms/ Communications
│ │ ├── services/ EmailService + 12 email functions + PDF gen
@@ -839,15 +961,39 @@ five-a-day/
│ │ ├── urls.py 10 URL patterns
│ │ └── management/commands/ send_email, test_all_emails
│ │
-│ ├── tests/ pytest suite (174 tests)
-│ └── conftest.py Shared fixtures
+│ ├── tests/ pytest suite (283 tests, 70 % coverage)
+│ └── conftest.py Shared fixtures (models + authenticated_client)
+│
+├── .github/ CI/CD — see docs/GITHUB.md
+│ ├── workflows/
+│ │ ├── ci.yml Lint + typecheck + tests on every push/PR
+│ │ ├── auto-merge.yml Hourly development → testing merge + PR to main
+│ │ ├── codeql.yml Weekly Python security scan
+│ │ └── notify-production.yml Email on push to main
+│ ├── dependabot.yml Weekly dependency updates
+│ └── CODEOWNERS Auto-request reviews from owner accounts
+│
+├── docs/
+│ ├── GITHUB.md Full CI/CD + branch protection reference
+│ ├── HTTPS.md HTTPS setup (Docker Nginx + Cloud Run)
+│ ├── UV.md UV dependency management guide
+│ ├── CELERY.md Celery worker/beat reference
+│ └── TODO.md Open tasks
│
-├── Dockerfile Multi-stage build
-├── docker-compose.yml PostgreSQL + Django
-├── Makefile 45+ commands
-├── pyproject.toml Dependencies (uv/pip compatible)
-├── CLAUDE.md AI development context
-└── DEPLOYMENT.md GCP deployment guide
+├── scripts/ Dev helpers (docker_smoke_test, etc.)
+├── backups/ DB dumps from `make backup` (gitignored)
+│
+├── Dockerfile Multi-stage build (builder + runtime)
+├── docker-compose.yml PostgreSQL + Redis + Django + Celery worker + beat
+├── docker-compose.testing.yml QA override (Gunicorn, DEBUG=False)
+├── Makefile 60+ commands (`make help`)
+├── pyproject.toml Dependencies (uv-managed) + tool config
+├── uv.lock Reproducible dependency lock
+├── entrypoint.sh Docker entrypoint (migrate, collectstatic, start)
+├── .env / .env.testing Gitignored — never committed
+├── CLAUDE.md AI development context (project rules)
+├── DEPLOYMENT.md GCP deployment guide (all 3 environments)
+└── README.md This file
```
### App: core
@@ -857,7 +1003,7 @@ Dashboard, authentication, scheduling, and shared utilities. Owns all views and
| Component | Details |
|-----------|---------|
| **Models** | TodoItem, HistoryLog (1000-entry cap), FunFridayAttendance, ScheduleSlot |
-| **Views** | 12 modules: auth, dashboard, students, parents, payments, management, app_forms, schedule, fun_friday_attendance, todos, support, errors |
+| **Views** | 13 modules: auth, dashboard, students, parents, payments, management, app_forms, schedule, fun_friday_attendance, todos, support, errors, testing_tools |
| **Middleware** | SimpleAuthMiddleware — session-based, protects all routes except /login/, /health/, /static/ |
| **Templates** | base.html (layout), 15+ page templates, 12 email templates, error pages |
| **Static** | app.css (sidebar/icons), 13 JS modules, logo |
@@ -1049,8 +1195,8 @@ Standalone page with custom styling (does not extend base.html).
| Metric | Value |
|--------|-------|
-| **Total tests** | 294 |
-| **Test files** | 17 |
+| **Total tests** | 283 |
+| **Test files** | 19 |
| **Coverage** | 70% (with `--cov-report=term-missing` on every run) |
| **Runtime** | ~30 seconds (8 parallel workers via pytest-xdist) |
| **Database** | PostgreSQL (same as production) — **always use `make test`** |
@@ -1207,7 +1353,7 @@ Production defaults are applied automatically when `DEBUG=False` — no manual o
- Django's `CsrfViewMiddleware` is active in the middleware stack.
- All POST endpoints receive CSRF validation. JavaScript AJAX requests use `getCsrfToken()` (reads from cookies) and send via `X-CSRFToken` header.
-- `CSRF_TRUSTED_ORIGINS` is configured per deployment (`render.yaml`, `gcp-cloudrun.yaml`).
+- `CSRF_TRUSTED_ORIGINS` is configured per deployment via the `CSRF_TRUSTED_ORIGINS` env var (see [DEPLOYMENT.md](DEPLOYMENT.md)).
- Only exception: `@csrf_exempt` on `/health/` endpoint (GET-only, returns `{"status": "healthy"}`).
### Transport Security (HTTPS)
@@ -1244,30 +1390,27 @@ All settings are environment-controlled and only activate when `DEBUG=False`.
| Health checks | Database has auth-checking healthcheck; web service uses `/health/` endpoint |
| Seed script guard | `scripts/reset_seed_dev_data.py` aborts if `DJANGO_ENV=production` or `DEBUG=False` |
-#### Render (render.yaml)
-
-| Decision | Implementation |
-|----------|---------------|
-| Auto-generated secrets | `DJANGO_SECRET_KEY` and `DJANGO_SUPERUSER_PASSWORD` use `generateValue: true` |
-| Dashboard-only secrets | `DJANGO_SUPERUSER_USERNAME`, `DJANGO_SUPERUSER_EMAIL`, `LOGIN_USERNAME`, `LOGIN_PASSWORD`, `EMAIL_HOST_USER`, `EMAIL_SECRET` use `sync: false` (set in Render dashboard, not in YAML) |
-| SSL enforced | `SECURE_SSL_REDIRECT=True`, all cookie secure flags enabled |
-| Strict cookies | `SESSION_COOKIE_SAMESITE=Strict`, `CSRF_COOKIE_SAMESITE=Strict`, `CSRF_COOKIE_HTTPONLY=True` |
+#### Google Cloud Run
-#### Google Cloud Run (gcp-cloudrun.yaml)
+Full deployment walkthrough in [DEPLOYMENT.md](DEPLOYMENT.md). Security-relevant decisions:
| Decision | Implementation |
|----------|---------------|
-| Secret Manager | All credentials (`DJANGO_SECRET_KEY`, `LOGIN_*`, `EMAIL_SECRET`, `POSTGRES_*`, `GOOGLE_*`) loaded from GCP Secret Manager via `secretKeyRef` |
-| Service account | Runs under dedicated `fiveaday-sa` service account with least-privilege IAM |
-| Autoscaling | min=0, max=3 instances; startup probe with 50s timeout |
+| Secret Manager | All credentials (`DJANGO_SECRET_KEY`, `LOGIN_*`, `EMAIL_SECRET`, `POSTGRES_*`, `GOOGLE_*`) injected at startup from GCP Secret Manager |
+| Cloud SQL Auth Proxy | PostgreSQL connection goes through the proxy — no public IP on the database |
+| Autoscaling | min=0 (cold starts acceptable) or min=1 (~$7/mo) for always-warm, max=2 instances |
| Probes | Startup probe + liveness probe on `/health/` |
+| TLS | Managed automatically by Cloud Run (custom domain + Google-managed certificate) |
+| SSL enforced | `SECURE_SSL_REDIRECT=True`, all cookie secure flags enabled when `DEBUG=False` |
+| Strict cookies | `SESSION_COOKIE_SAMESITE=Strict`, `CSRF_COOKIE_SAMESITE=Strict`, `CSRF_COOKIE_HTTPONLY=True` |
### Secrets Management
| Rule | Implementation |
|------|---------------|
| No hardcoded credentials | `auth.py` requires `LOGIN_USERNAME`/`LOGIN_PASSWORD` env vars — refuses login if missing |
-| No secrets in YAML | `render.yaml` uses `generateValue` or `sync: false`; `gcp-cloudrun.yaml` uses Secret Manager refs |
+| No secrets in YAML | Production credentials live in GCP Secret Manager, injected into Cloud Run at startup — never in the repo |
+| No secrets in GitHub Actions for deploy | CI uses only non-production Gmail SMTP + Codecov upload token. Production deploy runs manually with the operator's `gcloud` credentials |
| No secrets in Docker image | `.dockerignore` excludes all `.env*` files |
| `.gitignore` coverage | `.env*` pattern excludes all env file variants |
| Production startup validation | `settings.py` raises `ValueError` if `SECRET_KEY` is the dev default and `DEBUG=False` |
@@ -1463,66 +1606,7 @@ The `seed_testdata` command creates:
Use `--reset` to wipe and re-seed, or `--small` for a minimal dataset (6 children only).
-### GCP deployment plan
-
-The QA environment will be deployed on Google Cloud Platform, optimized for minimal cost:
-
-#### Recommended setup: Cloud Run + Cloud SQL
-
-| Component | GCP Service | Spec | Estimated cost |
-|-----------|-------------|------|----------------|
-| Application | Cloud Run | 1 vCPU, 512 MB, scales 0–2 | Free tier covers ~2M requests/month |
-| Database | Cloud SQL (PostgreSQL 16) | `db-f1-micro`, 10 GB SSD | ~$8/month |
-| Container images | Artifact Registry | Standard repo | Free tier (0.5 GB) |
-| HTTPS | Cloud Run managed | Automatic TLS certificate | Free |
-| DNS (optional) | Cloud DNS | 1 managed zone | ~$0.20/month |
-
-**Total estimated cost: ~$8–10/month**
-
-Cloud Run scales to zero when nobody is using it (no cost for idle time) and GCP provides automatic HTTPS with a `*.run.app` domain at no extra cost. The `db-f1-micro` Cloud SQL instance is the smallest available and more than enough for a QA team of 3–10 people.
-
-#### Why Cloud Run instead of a VM or Kubernetes
-
-- Kubernetes (GKE) has a management fee (~$70/month) that makes no sense for a small QA environment.
-- A Compute Engine VM would cost ~$5/month but requires manual updates, SSL certificate management, and doesn't scale to zero.
-- Cloud Run gives production-grade infrastructure (load balancing, HTTPS, health checks, rolling deploys) with almost no operational overhead.
-
-#### Deployment steps (run once during initial setup)
-
-```bash
-# 1. Build and push the Docker image
-gcloud builds submit --tag gcr.io/PROJECT_ID/fiveaday-testing
-
-# 2. Create the Cloud SQL instance
-gcloud sql instances create fiveaday-testing \
- --tier=db-f1-micro \
- --region=europe-southwest1 \
- --database-version=POSTGRES_16
-
-# 3. Create the database and user
-gcloud sql databases create fiveaday_testing --instance=fiveaday-testing
-gcloud sql users create fiveaday_tester --instance=fiveaday-testing --password=SECURE_PASSWORD
-
-# 4. Store secrets
-echo -n "value" | gcloud secrets create SECRET_NAME --data-file=-
-
-# 5. Deploy to Cloud Run
-gcloud run deploy fiveaday-testing \
- --image gcr.io/PROJECT_ID/fiveaday-testing \
- --region europe-southwest1 \
- --allow-unauthenticated \
- --set-env-vars "DJANGO_ENV=production,DJANGO_DEBUG=False" \
- --set-secrets "DJANGO_SECRET_KEY=django-secret-key:latest"
-
-# 6. Seed the database (one-time, via Cloud Run job or exec)
-gcloud run jobs create seed-testdata \
- --image gcr.io/PROJECT_ID/fiveaday-testing \
- --command "python" \
- --args "project/manage.py,seed_testdata" \
- --region europe-southwest1
-```
-
-After deployment, Cloud Run provides a URL like `https://fiveaday-testing-xxxxx.europe-southwest1.run.app` with HTTPS enabled automatically.
+> **Deploying the QA environment** — see [DEPLOYMENT.md](DEPLOYMENT.md) for the full GCP plan. Testing runs on a Compute Engine e2-micro (free tier) with Docker Compose, while production uses Cloud Run + Cloud SQL.
---
@@ -1544,6 +1628,7 @@ Auto-merge check
• development ahead of testing?
• last commit ≥ 24 h old?
• CI passing on that commit?
+ • version bumped in pyproject.toml (dev > testing)?
│ all yes
▼
git merge development → testing
@@ -1594,14 +1679,23 @@ Concurrent CI runs on the same branch cancel each other automatically — new pu
- CI triggers immediately (lint, typecheck, tests run in parallel, ~2-4 min)
- CodeQL triggers immediately (weekly scan also runs independently)
-- The hourly auto-merge cron checks this commit every hour until it is ≥ 24 h old with passing CI, then promotes to `testing`
+- The hourly auto-merge cron promotes to `testing` only when **all four** conditions hold: dev is ahead of testing, the last commit is ≥ 24 h old, CI is green, **and the version in `pyproject.toml` has been bumped** (strictly higher than `testing`'s version). Without a version bump the merge is skipped even with 24 h of new commits on dev — run `make pc-run` (answer yes) or `make version x.y.z` before the next tick to unlock it.
**2. Auto-merge fires**
- Creates a `--no-ff` merge commit on `testing` titled `YYYY-MM-DD - `
- Pushes to `testing` (which triggers CI on `testing`)
+- **Creates and pushes an annotated staging tag `testing-vX.Y.Z`** on the new testing merge commit
- Opens PR `testing → main` if one is not already open (title matches the merge commit)
-- Sends an HTML email to `OWNER_EMAILS` with a "Review PR" button
+- Sends an HTML email to `OWNER_EMAILS` with version bump, staging tag, and a "Review PR" button
+
+**2b. You merge the PR → release tag on main**
+
+- `notify-production.yml` reads `version` from `pyproject.toml` on `main`'s new HEAD
+- **Creates and pushes an annotated release tag `vX.Y.Z`** on that commit (skipped if tag already exists)
+- Sends an HTML email to `hellofiveaday@gmail.com` with the release tag and `gcloud` deploy steps
+
+The two tag namespaces (`testing-vX.Y.Z` and `vX.Y.Z`) are fully independent — the `testing → main` PR can use any merge strategy (merge commit, squash, or rebase) because the release tag is derived from `pyproject.toml`, not from commit SHA continuity.
**3. You review and merge the PR**
@@ -1723,19 +1817,20 @@ Results appear in **Security → Code scanning alerts**. A new alert on `main` d
```bash
# First-time setup
uv sync --no-install-project # Install all dependencies (UV — see docs/UV.md)
-make pre-commit-install # Install pre-commit hooks (Ruff + mypy + bandit)
-make up # Start Docker (PostgreSQL + Django)
+make pre-commit-install # Install the git pre-commit hook
+make up # Start Docker (PostgreSQL + Redis + Django + Celery)
```
-1. Create a feature branch from `development`
+1. Work on `development` (or a short-lived branch off `development`)
2. Make changes following the conventions below
-3. Run `make lint` — Ruff linting must pass
-4. Run `make mypy` — mypy type checking must pass
-5. Run `make test` — all 294 tests must pass (PostgreSQL via Docker, parallel, with coverage)
-6. Run `make check` — no Django system check issues
-7. Create a pull request with clear description of changes
+3. Run `make pc-run` — Ruff + mypy + bandit all pass, offers to auto-bump the patch version on success, and auto-stages `uv.lock` if regenerated
+4. Run `make test` — all 283 tests must pass (PostgreSQL via Docker, parallel, with coverage)
+5. `git commit` with a message like `v1.0.6 - Short description` (version comes first — conventions match every other commit in the project)
+6. `git push origin development`
+7. CI runs automatically on your push (see [CI/CD](#cicd--github-actions))
+8. ~24 h later, the auto-merge pipeline promotes your commit to `testing` and opens a PR to `main` for your review
-Pre-commit hooks run **Ruff** (lint + format), **mypy** (type checking), and **bandit** (security) automatically on every commit.
+Pre-commit hooks run **Ruff** (lint + format), **mypy** (type checking), and **bandit** (security) automatically on every `git commit`. If a hook modifies files (e.g. mypy regenerates `uv.lock`), the commit aborts — running `make pc-run` once resolves this by staging the regenerated lock file.
### Make Commands (Developer Tooling)
@@ -1749,7 +1844,8 @@ Pre-commit hooks run **Ruff** (lint + format), **mypy** (type checking), and **b
| **pytest-xdist** | Parallel test execution | Built into `make test` (`-n auto`) |
| **pytest-randomly** | Randomized test ordering | Built into `make test` (seed printed) |
| **pytest-cov** | Coverage reporting + badge | `make test`, `make coverage-badge` |
-| **pre-commit** | Git hooks: ruff, mypy, bandit | `make pre-commit-install` |
+| **pre-commit** | Git hooks: ruff, ruff-format, mypy, bandit | `make pre-commit-install` (first-time), `make pc-run` (dry-run all hooks + auto bump) |
+| **make version** | Bump version in both `pyproject.toml` and `settings.py` | `make version x.y.z` (positional, with `y/N` confirmation) |
All tools are configured in `pyproject.toml` and installed as dev dependencies via `uv sync`.
diff --git a/project/comms/README.md b/project/comms/README.md
index aa13302..a08218d 100644
--- a/project/comms/README.md
+++ b/project/comms/README.md
@@ -14,7 +14,7 @@ Generic email sending service with HTML template rendering and inline images.
- `send_bulk_emails(template_name, emails_data, ...)` — sends multiple emails with the same template
- `email_service` — singleton instance used throughout the project
-Templates live in `core/templates/emails/` and extend `emails/base_email.html`.
+Templates live in `core/templates/emails/` and extend `emails/base_email.html`. There are currently **14 email templates**: `happy_birthday`, `welcome_student`, `enrollment_child`, `enrollment_adult`, `fun_friday`, `payment_reminder`, `receipt_quarterly_child`, `receipt_adult`, `receipt_enrollment`, `vacation_closure`, `tax_certificate`, `monthly_report`, `newsletter`, plus the shared `base_email`.
### Email Functions (`comms/services/email_functions.py`)
@@ -24,7 +24,7 @@ Convenience functions for each email type. Each wraps `email_service.send_email(
| -------- | -------- | ------- |
| `send_birthday_email` | `happy_birthday` | Daily cron / manual |
| `send_welcome_email` | `welcome_student` | On student creation |
-| `send_enrollment_confirmation_email` | `enrollment_child` | On enrollment |
+| `send_enrollment_confirmation_email` | `enrollment_child` / `enrollment_adult` | On enrollment |
| `send_fun_friday_email` | `fun_friday` | Weekly manual |
| `send_payment_reminder_email` | `payment_reminder` | Monthly manual |
| `send_quarterly_receipt_email` | `receipt_quarterly_child` | Quarterly manual |
@@ -34,6 +34,8 @@ Convenience functions for each email type. Each wraps `email_service.send_email(
| `send_monthly_report` | `monthly_report` | Monthly manual |
| `generate_tax_certificate_pdf` | (HTML to PDF) | Called by tax certificate |
+The **newsletter** and **enrollment receipt** templates do not have dedicated convenience functions — they are triggered directly from the `/apps/newsletter/` and receipt form views in `core/views/app_forms.py`, which call `email_service.send_email()` inline with per-recipient context.
+
## Celery Tasks (`comms/tasks.py`)
All tasks have retry logic (3 retries, exponential backoff):
@@ -63,7 +65,7 @@ python manage.py send_email --tax-certificate --year 2024
### `test_all_emails`
```bash
-python manage.py test_all_emails # Send all 11 test emails
+python manage.py test_all_emails # Send one test of each email template
python manage.py test_all_emails --only fun_friday,birthday
python manage.py test_all_emails --list # List available templates
python manage.py test_all_emails --to admin@test.com
diff --git a/project/core/README.md b/project/core/README.md
index 94ed9de..f732db4 100644
--- a/project/core/README.md
+++ b/project/core/README.md
@@ -13,12 +13,12 @@ The `core` app is the "everything else" app — it owns the dashboard, authentic
## Views (core/views/)
-The monolithic `views.py` was split into 12 focused modules:
+The monolithic `views.py` was split into 13 focused modules:
| Module | Views | Description |
| ------ | ----- | ----------- |
| `auth.py` | `login_view`, `logout_view`, `google_oauth_redirect`, `google_oauth_callback` | Session-based auth + Google OAuth |
-| `dashboard.py` | `home`, `all_info` | Dashboard with stats (single `Case/When` aggregate query), todos, birthdays; database view |
+| `dashboard.py` | `home`, `all_info` | Dashboard with stats (single `Case/When` aggregate query), todos, birthdays, inspirational quote from zenquotes.io (48 h cookie); database view |
| `schedule.py` | `schedule_view`, `save_schedule_slot`, `fun_friday_view` | Weekly schedule grid + Fun Friday list (single attendance query for both weeks, filters from loaded students) |
| `fun_friday_attendance.py` | `toggle_fun_friday_this_week`, `add/remove_fun_friday_attendance` | AJAX attendance toggles |
| `todos.py` | `create_todo`, `complete_todo`, `history_list` | Todo CRUD + history pagination API |
@@ -26,19 +26,26 @@ The monolithic `views.py` was split into 12 focused modules:
| `parents.py` | `ParentCreateView` | Parent creation CBV |
| `payments.py` | `payments_list`, `create_payment`, `quick_complete_payment`, etc. | Payment CRUD + AJAX APIs. Stats use single `Case/When` aggregate (1 query instead of 8). |
| `management.py` | `gestion_view`, `update_site_config`, `create_teacher`, `create_group` | Admin config panel |
-| `app_forms.py` | `fun_friday_form`, `payment_reminder_form`, etc. (10 views) | Email app form views |
+| `app_forms.py` | `fun_friday_form`, `payment_reminder_form`, `newsletter_form`, `receipt_enrollment_form`, etc. | Email app form views (10+ forms, all prefill from `ACADEMY_*` env vars where relevant) |
| `support.py` | `submit_support_ticket` | Support ticket email API |
| `errors.py` | `handler400-500`, `health_check` | Error pages + health endpoint |
+| `testing_tools.py` | `testing_tools_view`, `seed_testdata_ajax`, `submit_backlog`, `toggle_error_reporting` | **QA-only** dashboard at `/testing/` — database seeding, backlog reporting, error-reporting toggle. All gated by `qa_access_required` decorator. |
## URL Patterns (core/urls.py)
-Routes for: login/logout, dashboard, schedule, todos, history, support, error test pages.
+Routes for: login/logout, dashboard, schedule, todos, history, support, `/testing/` QA dashboard, error test pages.
Student, payment, management, and email app routes live in `students/urls.py`, `billing/urls.py`, and `comms/urls.py` respectively, but their views are still in `core/views/`.
-## Middleware
+## Middleware & Decorators
-**SimpleAuthMiddleware** — session-based auth that protects all URLs except `/login/`, `/health/`, `/static/`, `/media/`, and `/auth/google/*` (including `/callback/`). Credentials come from `LOGIN_USERNAME`/`LOGIN_PASSWORD` env vars (required; no hardcoded fallbacks).
+- **`SimpleAuthMiddleware`** (`middleware.py`) — session-based auth that protects all URLs except `/login/`, `/health/`, `/static/`, `/media/`, and `/auth/google/*` (including `/callback/`). Credentials come from `LOGIN_USERNAME`/`LOGIN_PASSWORD` env vars (required; no hardcoded fallbacks).
+- **`QAErrorEmailMiddleware`** (`middleware.py`) — in the QA environment, catches unhandled exceptions and emails them to `SUPPORT_EMAIL` with the full traceback. Toggleable via the `/testing/` dashboard.
+- **`qa_access_required`** (`decorators.py`) — reusable gate for `/testing/` views and endpoints. Returns 404 (not 403) unless `DJANGO_ENV=testing`, `DEBUG=False`, and the session user matches `QA_TESTING_USERNAME`.
+
+## Management Commands
+
+- **`seed_testdata`** — populates the QA database with 3 teachers, 5 groups, 6 parents, 12 child students, 3 adult students, 1 inactive student, active enrollments, payments in various states, schedule slots, todo items, and history log entries. Flags: `--reset` (wipe first), `--small` (6 children only). Also callable from the `/testing/` dashboard via AJAX.
## Templates
diff --git a/project/project/settings.py b/project/project/settings.py
index 6c149b4..60feb82 100644
--- a/project/project/settings.py
+++ b/project/project/settings.py
@@ -12,10 +12,10 @@
# ============================================================================
# APP VERSION
# ============================================================================
-# NOTA: Al cambiar la versión, actualizar también en:
-# - readme.md (badge y texto)
+# NOTA: Usa `make version x.y.z` para actualizar ambos sitios a la vez:
# - pyproject.toml (campo version)
-APP_VERSION = os.getenv("APP_VERSION", "1.0.5")
+# - README.md (badge y tabla de versiones — gestionado por la skill update-readme)
+APP_VERSION = os.getenv("APP_VERSION", "1.0.6")
# ============================================================================
# SECURITY SETTINGS
diff --git a/pyproject.toml b/pyproject.toml
index 3d78a11..02303a3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "five-a-day"
-version = "1.0.5"
+version = "1.0.6"
description = "Five a Day management software"
readme = "README.md"
requires-python = ">=3.12"
diff --git a/uv.lock b/uv.lock
index ba23172..42a4ac3 100644
--- a/uv.lock
+++ b/uv.lock
@@ -695,7 +695,7 @@ wheels = [
[[package]]
name = "five-a-day"
-version = "1.0.5"
+version = "1.0.6"
source = { editable = "." }
dependencies = [
{ name = "celery" },
From efe1ec06e6609cd01559147594ef1845efaa5890 Mon Sep 17 00:00:00 2001
From: Joaquin Hernandez Martinez
Date: Wed, 15 Apr 2026 15:21:39 +0200
Subject: [PATCH 3/8] v1.0.7 - Fixed pipeline issues and made it work, as well
as handled config and pipeline colliding, and fixed some small issues
---
.github/workflows/ci.yml | 8 ++++
CLAUDE.md | 5 +-
Makefile | 24 ++++++++--
README.md | 80 +++++++++++++++++++++++++++----
project/core/README.md | 4 +-
project/core/static/favicon.ico | Bin 0 -> 70564 bytes
project/core/templates/base.html | 26 ++++++++++
project/core/views/dashboard.py | 17 +++++--
project/project/settings.py | 2 +-
project/project/settings_test.py | 10 ++++
project/pytest.ini | 2 +
project/static/favicon.ico | Bin 0 -> 70564 bytes
pyproject.toml | 2 +-
uv.lock | 2 +-
14 files changed, 161 insertions(+), 21 deletions(-)
create mode 100644 project/core/static/favicon.ico
create mode 100644 project/static/favicon.ico
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c5ed88c..0fe9bea 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -58,6 +58,14 @@ jobs:
run: uv sync --frozen --no-install-project
- name: mypy
+ env:
+ # django-stubs plugin imports project.settings at load time. Our settings.py
+ # raises ValueError when DEBUG=False AND SECRET_KEY is the dev default, so we
+ # set DJANGO_DEBUG=True to bypass that check during static analysis.
+ DJANGO_SETTINGS_MODULE: project.settings
+ DJANGO_DEBUG: "True"
+ DJANGO_SECRET_KEY: mypy-static-analysis-dummy-key
+ PYTHONPATH: project
run: uv run mypy project/
# ============================================================
diff --git a/CLAUDE.md b/CLAUDE.md
index 8114f8a..f1ff2fb 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -126,9 +126,12 @@ All pricing flows through `billing/services/`. The single source of truth is `Si
- **Celery eager mode** — without Redis, tasks run synchronously. Don't rely on task.delay() being truly async in development.
- **`#webcrumbs` removed** — the old CSS scoping wrapper is gone. If you see references to it in old code, delete them.
- **Template names are English** — all email templates were renamed from Spanish (e.g., `matricula_niño.html` → `enrollment_child.html`). Never create templates with Spanish names.
-- **Version in two places** — `pyproject.toml` and `project/project/settings.py`. Use `make version x.y.z` (positional, with y/N confirmation) to update both.
+- **Version in four places** — `pyproject.toml`, `project/project/settings.py` (`APP_VERSION` default), the `README.md` header badge URL, and `uv.lock` (the project's own `[[package]]` entry). `make version x.y.z` (positional, with y/N confirmation) updates the first three with `sed` and regenerates `uv.lock` via `uv lock --quiet`. Running `make version` with no argument prints the pyproject and README badge values side-by-side and warns if they've drifted. `make pc-run`'s auto patch-bump does the same four updates, then the existing `git add uv.lock` block at the end of the target stages the regenerated lockfile so the next commit isn't blocked.
- **APP_VERSION in `.env`** — the local `.env` may contain a legacy `APP_VERSION=0.x.y` line. Either remove the line or update it — it silently overrides the default in `settings.py` at runtime.
- **`pc-run` renamed** — the old `make pre-commit-run` target is now `make pc-run`. It also auto-bumps the patch version on a clean pass (y/N prompt) and auto-stages `uv.lock` if regenerated.
+- **mypy CI job needs `DJANGO_DEBUG=True`** — `django-stubs` imports `project.settings` at load time for the Django plugin. Without `DJANGO_DEBUG=True` + a dummy `DJANGO_SECRET_KEY`, the production guard at the top of `settings.py` raises `ValueError: DJANGO_SECRET_KEY debe ser cambiado en producción`. The CI `mypy` step sets both, plus `PYTHONPATH=project`, as env vars. Any new static-analysis job that imports settings will need the same.
+- **Test settings disable production security redirects** — `settings_test.py` explicitly sets `SECURE_SSL_REDIRECT = False`, `SECURE_HSTS_SECONDS = 0`, `SESSION_COOKIE_SECURE = False`, `CSRF_COOKIE_SECURE = False`. Don't remove them — the Django test client speaks HTTP against `testserver`, and inheriting `SECURE_SSL_REDIRECT=True` from `settings.py` (which kicks in when `DEBUG=False`) turns every test request into a 301 to `https://testserver/...`. The overrides keep the test settings self-contained regardless of how CI configures `DJANGO_DEBUG`.
+- **WhiteNoise warning is filtered** — `pytest.ini` has `filterwarnings = ignore:No directory at:UserWarning` to silence the once-per-request warning from WhiteNoise middleware when `STATIC_ROOT` (`staticfiles/`) doesn't exist. That directory only exists after `collectstatic` runs (production only), so the warning is noise in tests. Don't add `collectstatic` to the test command.
## README maintenance (MUST do at end of every work session)
diff --git a/Makefile b/Makefile
index e9581bf..07509b3 100644
--- a/Makefile
+++ b/Makefile
@@ -363,20 +363,35 @@ endif
version:
@CURRENT=$$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/'); \
+ BADGE=$$(grep -oE 'version-v[0-9]+(\.[0-9]+)*-brightgreen' README.md | head -1 | sed -E 's/version-v(.*)-brightgreen/\1/'); \
NEW="$(_VERSION_ARG)"; \
if [ -z "$$NEW" ]; then \
echo "Usage: make version x.y.z"; \
echo ""; \
- echo "Current version: $$CURRENT"; \
+ echo "Current version:"; \
+ echo " pyproject.toml: $$CURRENT"; \
+ echo " README.md badge: $$BADGE"; \
+ if [ -n "$$BADGE" ] && [ "$$CURRENT" != "$$BADGE" ]; then \
+ echo ""; \
+ echo " WARNING: pyproject and README badge are out of sync."; \
+ fi; \
exit 1; \
fi; \
read -p "Version $$CURRENT will become the new version $$NEW, are you sure? [y/N] " confirm; \
if [ "$$confirm" = "y" ] || [ "$$confirm" = "yes" ]; then \
sed -i 's/^version = ".*"/version = "'"$$NEW"'"/' pyproject.toml; \
sed -i 's/APP_VERSION = os.getenv("APP_VERSION", ".*")/APP_VERSION = os.getenv("APP_VERSION", "'"$$NEW"'")/' project/project/settings.py; \
+ sed -i -E 's|version-v[0-9]+(\.[0-9]+)*-brightgreen|version-v'"$$NEW"'-brightgreen|' README.md; \
+ uv lock --quiet; \
echo "Version updated to $$NEW in:"; \
echo " - pyproject.toml"; \
- echo " - project/settings.py"; \
+ echo " - project/project/settings.py"; \
+ echo " - README.md (badge URL)"; \
+ echo " - uv.lock (regenerated via 'uv lock')"; \
+ echo ""; \
+ echo "NOTE: the Recent Versions table, Version History details block, and per-app"; \
+ echo " READMEs were NOT changed automatically — run the 'update-readme' skill"; \
+ echo " after staging your work to refresh them."; \
else \
echo "Cancelled."; \
fi
@@ -485,7 +500,10 @@ pc-run:
NEW="$$MAJOR.$$MINOR.$$((PATCH + 1))"; \
sed -i 's/^version = ".*"/version = "'"$$NEW"'"/' pyproject.toml; \
sed -i 's/APP_VERSION = os.getenv("APP_VERSION", ".*")/APP_VERSION = os.getenv("APP_VERSION", "'"$$NEW"'")/' project/project/settings.py; \
- echo "Updated version $$CURRENT with new version $$NEW"; \
+ sed -i -E 's|version-v[0-9]+(\.[0-9]+)*-brightgreen|version-v'"$$NEW"'-brightgreen|' README.md; \
+ uv lock --quiet; \
+ echo "Updated version $$CURRENT with new version $$NEW (pyproject.toml, settings.py, README badge, uv.lock)"; \
+ echo "Reminder: Recent Versions + Version History in README were NOT touched - run '/update-readme' skill to refresh them."; \
fi; \
fi
@if [ -n "$$(git status --porcelain uv.lock 2>/dev/null)" ]; then \
diff --git a/README.md b/README.md
index dd398de..bc9c590 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-
+
@@ -40,9 +40,9 @@ Live status for each environment is pulled from GitHub Actions — the badges be
| Version | Date | Description |
|---------|------|-------------|
-| **v1.0.5** | 2026-04-15 | GitHub Actions CI/CD pipeline (lint, typecheck, tests, CodeQL, Dependabot), auto-merge `development` → `testing` with 24 h delay + auto-PR to `main`, email notifications, branch protection rules for public repo hardening, `make pc-run` auto-stages regenerated `uv.lock` |
-| v1.0.4 | 2026-04-15 | Inspirational quote generator on `/home` (48 h cookie rotation), GCP deployment plan ([DEPLOYMENT.md](DEPLOYMENT.md)), Celery worker + beat containers, cleaned legacy Render config, `make version x.y.z` positional arg with confirmation guard, `make pc-run` (renamed from `pre-commit-run`) with auto version bump |
-| v1.0.3 | 2026-04-14 | Test coverage raised to **70 %** — 13 new test files across auth views, comms services, app forms, constants, create payment views, exports, forms, parent views, payment views, schedule views, student forms, student views, transactions |
+| **v1.0.7** | 2026-04-15 | Favicon + Open Graph / Twitter Card / `theme-color` / `apple-touch-icon` metadata on every page (multi-resolution `favicon.ico` generated from `logo.png`, overridable per-page blocks); `SECURE_SSL_REDIRECT`/HSTS/secure cookies disabled in `settings_test.py` so the CI test suite stops getting 301-redirected by the Django test client; WhiteNoise `No directory at: staticfiles/` warning silenced via `pytest.ini` `filterwarnings`; dashboard quote fetcher now uses `follow_redirects=True` against `zenquotes.io/api/quotes` (no trailing slash) and logs failures instead of swallowing them; CI mypy job now sets `DJANGO_DEBUG=True` + dummy `DJANGO_SECRET_KEY` so django-stubs can import `settings.py` without tripping the production secret-key guard; `make version` + `make pc-run` now also rewrite the README version badge, regenerate `uv.lock`, and warn when `pyproject.toml` and the badge drift apart |
+| v1.0.6 | 2026-04-15 | New `update-readme` Claude skill under `.claude/skills/update-readme/SKILL.md` that routes staged changes across the whole documentation set (main README, `CLAUDE.md`, `DEPLOYMENT.md`, `docs/`, per-app READMEs); sweeping README restructure — renamed `readme.md` → `README.md`, reorganized sections, expanded env-var reference; `.env.testing.example` removed (its contents now live inline in the README's `.env template` code block, with `.gitignore` tightened so no `.env*` file can be committed); `auto-merge.yml` and `notify-production.yml` workflow refinements; per-app README tune-ups for `core` and `comms` |
+| v1.0.5 | 2026-04-15 | GitHub Actions CI/CD pipeline (lint, typecheck, tests, CodeQL, Dependabot), auto-merge `development` → `testing` with 24 h delay + auto-PR to `main`, email notifications, branch protection rules for public repo hardening, `make pc-run` auto-stages regenerated `uv.lock` |
---
@@ -70,6 +70,7 @@ Live status for each environment is pulled from GitHub Actions — the badges be
- [Frontend](#frontend)
- [Infrastructure \& Deployment](#infrastructure--deployment)
- [Python Dependencies](#python-dependencies)
+ - [Developer Tooling](#developer-tooling)
- [Database Schema](#database-schema)
- [ER Diagram](#er-diagram)
- [Key Constraints](#key-constraints)
@@ -106,6 +107,7 @@ Live status for each environment is pulled from GitHub Actions — the badges be
- [Model Tests](#model-tests)
- [Service Tests](#service-tests)
- [View Tests](#view-tests)
+ - [Additional Test Files (v1.0.0+)](#additional-test-files-v100)
- [Migrations](#migrations)
- [Security](#security)
- [Authentication](#authentication)
@@ -128,7 +130,7 @@ Live status for each environment is pulled from GitHub Actions — the badges be
- [How to report a problem](#how-to-report-a-problem)
- [Error pages you might see](#error-pages-you-might-see)
- [For developers: how the QA environment works](#for-developers-how-the-qa-environment-works)
- - [Access control for /testing/](#access-control-for-testing)
+ - [Access control for `/testing/`](#access-control-for-testing)
- [CI/CD \& GitHub Actions](#cicd--github-actions)
- [Pipeline Overview](#pipeline-overview)
- [Branch Strategy](#branch-strategy)
@@ -143,6 +145,7 @@ Live status for each environment is pulled from GitHub Actions — the badges be
- [CodeQL Security Scanning](#codeql-security-scanning)
- [Contributing](#contributing)
- [Development Workflow](#development-workflow)
+ - [Make Commands (Developer Tooling)](#make-commands-developer-tooling)
- [Code Conventions](#code-conventions)
- [Adding a Feature](#adding-a-feature)
- [License](#license)
@@ -151,8 +154,64 @@ Live status for each environment is pulled from GitHub Actions — the badges be
## Version History & Roadmap
-
-v1.0.5 — CI/CD Pipeline & Public Repo Hardening (current)
+
+v1.0.7 — Favicon, Social Metadata & CI Test Fixes (current)
+
+**Social sharing & branding**
+
+- Multi-resolution `favicon.ico` (16/32/48/64/128/256) generated from `project/static/images/logo.png` — dropped in both `project/static/` and `project/core/static/` so both STATICFILES_DIRS paths serve it
+- `base.html` now includes full social-sharing metadata: ``, ``, `` (matches the violet palette), `apple-touch-icon`, full Open Graph set (`og:type`, `og:site_name`, `og:title`, `og:description`, `og:image`, `og:image:alt`, `og:url`, `og:locale`), and Twitter Card summary tags
+- Every content field is wrapped in an overridable Django block (`meta_description`, `og_title`, `og_description`, `og_image`, `twitter_title`, `twitter_description`, `twitter_image`) so per-page templates can tailor link previews without touching `base.html`
+
+**Test-suite fixes**
+
+- `settings_test.py` now explicitly sets `SECURE_SSL_REDIRECT = False`, `SECURE_HSTS_SECONDS = 0`, `SESSION_COOKIE_SECURE = False`, `CSRF_COOKIE_SECURE = False` — the CI environment runs with `DJANGO_DEBUG=False`, which activated the production SSL redirect and turned every test request into a 301 to `https://testserver/...`. The test settings are now self-contained and correct regardless of `DJANGO_DEBUG`.
+- `pytest.ini` adds `filterwarnings = ignore:No directory at:UserWarning` to silence the 142 WhiteNoise warnings that were emitted once per test request (the `staticfiles/` directory only exists after `collectstatic`, which isn't run before tests)
+
+**Dashboard reliability**
+
+- Zenquotes fetch in `core/views/dashboard.py` now targets `https://zenquotes.io/api/quotes` (no trailing slash — the old URL was getting 301-redirected) with `follow_redirects=True` as a guard against future URL changes
+- Silent `except Exception: pass` replaced with proper `logger.warning(...)` calls — failures are still non-fatal but now visible in logs
+
+**CI tooling**
+
+- `mypy` job in `ci.yml` now sets `DJANGO_SETTINGS_MODULE=project.settings`, `DJANGO_DEBUG=True`, a dummy `DJANGO_SECRET_KEY`, and `PYTHONPATH=project` — `django-stubs` imports `settings.py` at load time, which previously raised the production secret-key guard
+- `make version x.y.z` now also updates the README version badge via `sed`, regenerates `uv.lock` via `uv lock --quiet`, and prints a reminder to run the `update-readme` skill afterwards; running `make version` with no arg now shows both `pyproject.toml` and the README badge side-by-side and warns if they've drifted
+- `make pc-run`'s auto patch-bump now also rewrites the README badge and regenerates `uv.lock` — the existing `git add uv.lock` tail stages the refreshed lockfile automatically
+
+
+
+
+v1.0.6 — Documentation Skill & Doc Overhaul
+
+**Documentation agent**
+
+- New `update-readme` Claude skill at `.claude/skills/update-readme/SKILL.md` — routes staged files to the right docs (main README, CLAUDE.md, DEPLOYMENT.md, docs/, per-app READMEs), applies per-file checklists, and sweeps for stale references across the full documentation tree
+
+**README overhaul**
+
+- `readme.md` → `README.md` rename (case-sensitive file systems matter on GCP)
+- Major reorganization of sections; expanded Environment Variables Reference; tightened Recent Versions table to 3 rows; populated Developer Tooling and Make Commands tables
+- `.env template` is now the single authoritative source for local env-var structure, lives inline in the README as a fenced `bash` block
+
+**Secrets hygiene**
+
+- Removed `.env.testing.example` (its content now lives only inline in the README `.env template` block)
+- `.gitignore` tightened: `.env*` matches everything, no `!.env.example` exception, no `.env*.example` carve-outs
+
+**CI workflow refinements**
+
+- `auto-merge.yml` — improved commit detection and PR creation for the `development` → `testing` → `main` cascade
+- `notify-production.yml` — richer production deployment notification email with commit info and next-step `gcloud` commands
+
+**Per-app docs**
+
+- `project/core/README.md` and `project/comms/README.md` touched up to match post-refactor structure
+
+
+
+
+v1.0.5 — CI/CD Pipeline & Public Repo Hardening
**GitHub Actions CI/CD** (new — see [docs/GITHUB.md](docs/GITHUB.md))
@@ -777,8 +836,8 @@ Run `make` or `make help` for the full list. Key commands:
| `make test-fast` | Stop on first failure |
| `make test-k K=payment` | Run tests matching keyword |
| **Versioning** | |
-| `make version 1.1.0` | Update version in `pyproject.toml` + `settings.py` (with y/N confirmation) |
-| `make version` | Show current version |
+| `make version 1.1.0` | Update version in `pyproject.toml`, `settings.py`, the README badge, and regenerate `uv.lock` (with y/N confirmation); reminds you to run the `update-readme` skill to refresh Version History |
+| `make version` | Show current version from `pyproject.toml` + README badge; warns if they've drifted |
| **Developer Tooling** | |
| `make lint` / `make lint-fix` | Run Ruff linter (optionally auto-fix) |
| `make format` / `make format-check` | Run Ruff formatter |
@@ -803,8 +862,9 @@ The project supports three environments, controlled by `DJANGO_ENV` and `DJANGO_
| Environment | `DJANGO_ENV` | `DJANGO_DEBUG` | Database | Static Files | Use Case |
|------------|-------------|---------------|----------|-------------|----------|
| **Production** | `production` | `false` | PostgreSQL (Cloud SQL) | WhiteNoise + collectstatic | Live deployment |
-| **Development** | `development` | `true` | PostgreSQL (Docker) | Django dev server | Local coding |
| **Testing** | (via settings_test.py) | `false` | PostgreSQL (Docker) | Simple storage | `make test` |
+| **Development** | `development` | `true` | PostgreSQL (Docker) | Django dev server | Local coding |
+
> **Defaults are production-safe**: `DJANGO_DEBUG` defaults to `false` and `DJANGO_ENV` defaults to `development`. In production, always set `DJANGO_ENV=production` and ensure `DJANGO_SECRET_KEY` is a strong random value.
diff --git a/project/core/README.md b/project/core/README.md
index f732db4..09dfe04 100644
--- a/project/core/README.md
+++ b/project/core/README.md
@@ -51,7 +51,7 @@ Student, payment, management, and email app routes live in `students/urls.py`, `
All templates live in `core/templates/`:
-- `base.html` — main layout (sidebar, header, support modal, Tailwind CDN config)
+- `base.html` — main layout (sidebar, header, support modal, Tailwind CDN config). Also carries the site-wide `` metadata: favicon + apple-touch-icon, `theme-color` (violet `#6d28d9`), meta description/author, full Open Graph set, and Twitter Card tags. Every content field is wrapped in a Django block (`meta_description`, `og_title`, `og_description`, `og_image`, `twitter_title`, `twitter_description`, `twitter_image`) so per-page templates can tailor link previews.
- `home.html`, `login.html`, `schedule.html`, `fun_friday.html`, etc.
- `payments/` — payment list, create, detail
- `apps/` — email form views + `_email_preview.html` partial
@@ -60,6 +60,8 @@ All templates live in `core/templates/`:
## Static Files
+- `favicon.ico` — multi-resolution (16/32/48/64/128/256) icon generated from `images/logo.png`; referenced from `base.html` as both `rel="icon"` and `rel="shortcut icon"`
+- `images/logo.png` — 500×500 PNG; reused as `apple-touch-icon` and Open Graph image
- `css/app.css` — sidebar transitions, Material Symbols icon font settings
- `js/base.js` — notification/history dropdowns (loaded on every page)
- `js/support.js` — support ticket modal
diff --git a/project/core/static/favicon.ico b/project/core/static/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..7979c4028f9d23153d0cdfdb4e27275a60fbf864
GIT binary patch
literal 70564
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