feat: build headless theatre laboratory website

This commit is contained in:
bisco
2026-06-22 19:42:43 +02:00
parent cae9180bc6
commit cff9a17c3c
65 changed files with 8994 additions and 128 deletions
+56
View File
@@ -0,0 +1,56 @@
# ADR-0001: Headless Wagtail with an Astro frontend
Date: 2026-06-22
Status: Accepted
## Context
The theatre workshop needs an editorially polished public single page whose core
content can be updated without code changes. The required stack is Wagtail/Django,
Astro, PostgreSQL, and Docker Compose. A small team must be able to run and maintain
the system locally without unnecessary services.
## Decision
Use Wagtail as a headless CMS backed by PostgreSQL and Astro as the public frontend.
Wagtail owns uploaded media and exposes a read-only aggregate endpoint at
`/api/site/home/`. Astro renders the page server-side in development/build time and
uses curated Italian fallback content when the CMS is temporarily unavailable.
Docker Compose runs PostgreSQL, Django, and Astro on one private default network.
The content model uses one Wagtail `HomePage` with three ordered feature cards, site
settings, and reusable snippets for teacher details, lesson details, shows, and gallery
items. This keeps editing simple and avoids a custom frontend state layer.
## Consequences
- Editors use the authenticated Wagtail admin while the public API remains read-only.
- The public page needs only one API request and degrades gracefully during CMS outages.
- Uploaded media and PostgreSQL data require separate persistent volumes and backups.
- Local frontend rendering depends on container networking; browser-facing media URLs
are generated from the configured public Wagtail base URL.
## Alternatives considered
- Calling the standard Wagtail endpoints separately was rejected because it adds
frontend orchestration for a single page.
- Embedding the frontend in Django templates was rejected because Astro is required.
- A JavaScript SPA and client-side state library were rejected as unnecessary.
## Security impact
The aggregate endpoint exposes published editorial content only. Wagtail admin keeps
its normal authentication and CSRF protections. Services bind to loopback for local
development, containers are not privileged, and secrets are supplied through the
environment.
## Operational impact
Operators must back up the PostgreSQL and media volumes. CMS schema changes require
normal Django migrations. Health checks order local startup.
## Rollback
Revert the feature commit and remove the Compose volumes only if their stored content
is no longer needed. Keep database and media backups when rolling back code alone.
+17 -9
View File
@@ -1,13 +1,21 @@
# Architecture
Describe the project architecture here.
The system has three runtime components on the Docker Compose default network:
Include:
1. Astro renders the public single page and requests one aggregate JSON document.
2. Wagtail/Django manages editorial content, serves media in development, and exposes
the read-only `/api/site/home/` endpoint.
3. PostgreSQL persists Wagtail content and metadata.
- main components;
- runtime dependencies;
- data flow;
- persistence;
- external integrations;
- deployment topology;
- relevant ADRs.
The aggregate response contains `settings`, `homepage`, `feature_cards`, `teacher`,
`lesson_info`, `shows`, and `gallery_items`. Image values contain browser-facing URLs
and alternative text. Only published home content and records marked for home display
are exposed. Astro validates/normalizes the response and falls back to checked-in
Italian demo content if fetching fails.
Wagtail's authenticated admin remains the only editing interface. The public frontend
has no database access and no public write API. Uploaded media is stored in a dedicated
Compose volume.
The deployment topology and content model are recorded in
[ADR-0001](adr/0001-headless-wagtail-astro.md).
+39 -11
View File
@@ -1,15 +1,43 @@
# Deployment
Describe how this project is deployed.
## Local environment
Include:
Copy `.env.example` to `.env`, replace the development placeholders, then run:
- environments;
- Docker/Compose usage;
- required configuration;
- secrets handling;
- exposed ports;
- volumes;
- networks;
- deployment commands;
- rollback procedure.
```bash
cp .env.example .env
docker compose up --build -d
docker compose ps
docker compose exec backend python manage.py seed_demo
```
The backend applies migrations and collects static files before starting Gunicorn.
All services have health checks; wait for healthy status before opening the site.
Compose exposes the Astro site on loopback port `4321` and Django/Wagtail on loopback
port `8000`. PostgreSQL is available only on the Compose network. `postgres_data` and
`media_data` are persistent named volumes.
The stack uses explicit PostgreSQL 16.9, Python 3.12.12, and Node.js 22.20 image
versions. Containers are not privileged and use `no-new-privileges`.
Required runtime variables are `DATABASE_URL`, `DJANGO_SECRET_KEY`, `DJANGO_DEBUG`,
`DJANGO_ALLOWED_HOSTS`, `WAGTAILADMIN_BASE_URL`, and `PUBLIC_CMS_API_URL`. PostgreSQL
bootstrap variables are also documented in `.env.example`. Do not use the example
credentials outside local development.
`WAGTAILADMIN_BASE_URL` must be browser-reachable because it is used for media URLs.
`PUBLIC_CMS_API_URL` must be reachable by Astro; within Compose it is
`http://backend:8000`.
## Production boundary
The Compose stack is a local development deployment. A public environment still needs
TLS termination, production static/media serving, restricted admin access, managed
secrets, off-host backups, monitoring, and an explicit domain/allowed-host policy.
## Rollback
Revert the application commit and rebuild images. Keep the database and media volumes
unless content deletion is intentional. Schema rollback must be evaluated per Django
migration; take coordinated database and media backups first.
+40 -9
View File
@@ -1,13 +1,44 @@
# Operations
Describe operational procedures.
## Routine commands
Include:
```bash
docker compose up --build -d
docker compose ps
docker compose logs -f backend frontend postgres
docker compose down
```
- startup and shutdown;
- health checks;
- logs;
- monitoring;
- backup and restore;
- routine maintenance;
- known operational risks.
Compose health checks PostgreSQL and Django. The public health endpoint is
`http://localhost:8000/health/`; the aggregate content endpoint is
`http://localhost:8000/api/site/home/`.
Apply schema changes with `docker compose exec backend python manage.py migrate`.
Create editors with `createsuperuser`; use Wagtail permissions for later non-superuser
accounts.
The backend applies migrations at container startup. To permanently reset local
state, use `docker compose down --volumes`; this deletes both database and media.
## Backup and restore
Back up the database and media at the same logical point in time:
```bash
mkdir -p backups/media
docker compose stop frontend backend
docker compose exec -T postgres sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > backups/database.sql
docker compose cp backend:/app/media/. backups/media/
docker compose start backend frontend
```
Restore is destructive: verify the backup in a disposable environment and take a fresh
backup first. Then stop both applications, recreate and restore the database, replace
the media content, restart, and verify health, API, admin login, and several images.
Keep real backups encrypted and off-host with a defined retention policy.
## Known risks
- Frontend fallback content can mask a CMS outage, so monitor backend health directly.
- Local named volumes are not off-host backups.
- Development media serving is unsuitable for production traffic.
+26 -10
View File
@@ -1,19 +1,35 @@
# Runbook
Operational runbook for this project.
## Site shows fallback content
## Common tasks
1. Check `docker compose ps` and the backend health status.
2. Request `http://localhost:8000/api/site/home/` directly.
3. Inspect `docker compose logs backend postgres`.
4. Verify `PUBLIC_CMS_API_URL` resolves from the frontend container.
5. Restart the frontend after recovery so its static page is rebuilt from CMS data.
Document routine operational tasks here.
## Wagtail does not start
## Troubleshooting
1. Confirm PostgreSQL is healthy.
2. Check `DATABASE_URL` and allowed hosts without printing secret values in shared logs.
3. Run `docker compose exec backend python manage.py migrate`.
4. Review backend logs for a specific migration or configuration error.
Document known issues, symptoms, checks, and remediation steps.
## Images are missing
1. Verify the `media_data` volume is mounted at `/app/media`.
2. Check `WAGTAILADMIN_BASE_URL` uses a URL reachable by the browser.
3. Confirm the image still exists in Wagtail and has not been removed from the volume.
## A service is unhealthy
1. Run `docker compose ps` and inspect `docker compose logs --tail=200 SERVICE`.
2. For PostgreSQL, check volume capacity and database/user configuration.
3. For the backend, check `/health/`, migrations, and database connectivity.
4. For the frontend, request port `4321`, then check the API separately because
fallback content can mask a CMS outage.
## Rollback
Document rollback procedures here.
## Emergency contacts
Document project-specific escalation paths if appropriate.
Revert the application commit and rebuild containers. Preserve database/media volumes.
Before reversing migrations or deleting volumes, make and validate coordinated backups.
+20 -13
View File
@@ -1,16 +1,23 @@
# Security
Describe security assumptions and controls.
- Wagtail admin uses Django authentication, authorization, sessions, CSRF protection,
and password hashing. The public site does not add accounts or write endpoints.
- The aggregate API is intentionally unauthenticated and returns published editorial
content only; secrets and unpublished drafts must never be serialized.
- PostgreSQL has no host-published port. Web ports bind to loopback for local use.
- Containers are unprivileged at the Compose level: no privileged mode, host network,
Docker socket, or added capabilities are used. `no-new-privileges` is enabled and
the application images run as non-root users.
- `.env` is ignored. `.env.example` contains replaceable development placeholders,
never production credentials. Use a deployment secret manager outside local use.
- `DJANGO_DEBUG` must be false and allowed hosts explicit outside development. Add TLS
at the edge before public exposure.
- Database and uploaded media backups may contain personal data. Restrict, encrypt,
retain, and delete them according to the operator's privacy policy.
- Avoid placing personal phone numbers or private contact details in logs. The API
legitimately exposes only contact details approved for publication.
- Dependency and image versions are explicit. Operators remain responsible for patch
upgrades, vulnerability scans, and production digest pinning.
Include:
- authentication;
- authorization;
- network exposure;
- TLS/certificates;
- secrets management;
- logging of sensitive data;
- container privileges;
- filesystem permissions;
- dependency management;
- relevant ADRs.
Manual production hardening remains required for reverse proxy headers, TLS, media
storage, backup retention, monitoring, and admin network policy.
+16 -11
View File
@@ -1,23 +1,28 @@
# Testing
Describe how tests are executed.
All tests should run inside Docker containers.
## Canonical test command
```bash
CHANGE_ME
docker compose run --rm backend python manage.py test
docker compose run --rm frontend npm run check
docker compose run --rm frontend npm run build
docker compose config --quiet
```
## Test categories
Describe applicable categories:
The test suite covers:
- unit tests;
- integration tests;
- linting;
- formatting checks;
- Ansible syntax checks;
- Docker/Compose validation;
- smoke tests.
- Django model and API integration tests;
- Astro static type and template validation;
- production frontend build;
- Docker Compose configuration validation.
All commands run in containers. The backend test container starts PostgreSQL through
Compose; the frontend checks use the checked-in lockfile for reproducible installs.
After seeding, smoke-test `/health/`, `/api/site/home/`, and the public page. Basic
keyboard navigation, responsive layout, and accessible names require a manual browser
pass until browser automation is added.