The landing page and backend for waterpark.dkrz.de, the HEALPix data hub on S3.
Multiple things live here. The landing page is a mkdocs-material site
published to gh-pages and served by nginx on the VM. The API is a
small FastAPI service behind the same nginx, serving /api.
The grid-doctor package and its technical documentation live in
freva-org/grid-doctor. A few
pages are shared between the two sites; see Shared pages.
landingpage/ |
mkdocs site: content, theme overrides, build tooling |
catalogue/ |
crawler configs, and the service that delivers what CI publishes |
.dev/ |
development stack: compose file, proxy config, secrets |
justfile |
entry point for everything below; just lists every recipe |
Everything runs in containers behind a proxy, so development is
same-origin exactly like production behind nginx. The only thing to
install is just (nix-shell -p just,
conda install -c conda-forge just, pip install rust-just, or your
package manager).
just dev # foreground, Ctrl-C stops it
just up # same, detachedhttp://localhost:8000 |
the site, with /api on the same origin |
http://localhost:8000/mailbox/ |
every outgoing email lands here |
Using podman: COMPOSE=podman-compose just dev, or export COMPOSE once
in your shell. Rootless is fine for either runtime.
Nothing you send in development leaves your machine. The API talks to a
mailpit container that accepts every message and delivers none, so you can
sign up with real addresses all day without mailing anyone. Mailpit is not
published on a port of its own; like everything else it is reached through
the proxy, at /mailbox/.
A confirmation link in a message there is clickable and takes you to
/newsletter/?status=success, which is the whole double opt-in flow.
To check without opening the UI, just mail prints what listmonk logged
alongside what mailpit actually holds. If those two disagree, you know
which side to look at.
Optional, and the only configuration this stack has. Write one file per
setting into .dev/secrets/. The filename is the setting name, the
contents are the value.
With that directory empty, which is the default and how CI runs, everything goes to the mailbox.
Only the proxy is published. Everything else talks over the waterpark
bridge by service name, which is what keeps development same-origin:
nothing can reach the API on a second port and start needing CORS.
| service | what it is |
|---|---|
proxy |
Caddy. /api/* to the API, /mailbox/* to mailpit, the rest to the docs |
docs |
mkdocs serve with live reload; data, shared, assets mounted read-only |
mailbox |
mailpit |
Source is mounted read-only and everything the containers write goes to named volumes, so nothing lands root-owned in your checkout.
Editing anything under landingpage/data, landingpage/shared.
Adding a new file to the docs needs one service restarted:
just restart docsThe docs container assembles .build/data at startup as a farm of
symlinks into the three source trees, which is why edits are picked up but
new files are not. Never edit anything under .build/; it is generated.
just logs listmonk # follow one service (no argument: all of them)
just shell docs # shell inside a container
just restart docs # restart one or more services
just ps # what is running
just mail # what the mailbox has caught
just announce # seed a test announcement so the banner shows
just rebuild # rebuild images ignoring the layer cache
just down # stop, keep volumes
just clean # stop, remove images, KEEP the database
just seed-dump # save listmonk's settings and lists as the dev seed
just nuke # delete everything, database included (asks first)
just check # everything CI runs for the docs
just doctor # dump everything needed to diagnose a broken stackThe docs site has its own justfile in landingpage/, usable on its own
(cd landingpage && just serve) or from here as a module:
just site::serve # docs alone on :8000, no listmonk or mailbox
just site::build # strict build into landingpage/siteStart with just doctor. It prints the parsed compose config, the
container list including exited ones, published ports, recent logs from
proxy, docs, listmonk and the mailbox, and what is actually in .build/data.
- A 404 in the browser. If the page says "This response came from Caddy", the proxy could not reach an upstream container. If it does not, the 404 came from mkdocs or the API and the proxy is fine.
- Nothing on
:8000at all. Check for a stray process holding the port:pgrep -af "mkdocs serve". - Emails not arriving. They are in the mailbox, not your inbox.
just mailsettles it either way. - No log output from the API. The application logger defaults to
ERROR, which hides every "verification sent" and "address rejected" line. The stack setsVERBOSITY=2(INFO);3is DEBUG. - The STAC or data browser page 404s. Those two pages are generated at
container start by scripts that clone and run npm. The step is
deliberately non-fatal so the rest of the site still works when it
breaks.
SKIP_ASSET_BUILD=1 just devskips it entirely, which confirms whether that is what is failing.
landingpage/shared/ holds pages owned by grid-doctor and rendered by both
sites. They are vendored at a pinned commit rather than fetched at build
time, so builds work offline and an upstream change arrives as a reviewable
diff.
cd landingpage
python scripts/sync_shared.py --check # CI guard: did someone edit them here?
python scripts/sync_shared.py --update # re-pin to grid-doctor's tipThe pin lives in landingpage/shared-sources.toml. A scheduled workflow
bumps it weekly and opens a pull request. Edit these pages upstream, never
here: a local change is reverted by the next sync, and --check fails the
build in the meantime.
The banner across the top of the site is served at runtime from
/api/announcements, backed by a TOML file on the host. Editing that file
is the whole publishing workflow: no rebuild, no workflow run, no restart.
Every entry needs an expires, deliberately. A notice that cannot clear
itself is how the previous banner got stuck; anything true indefinitely is
documentation and belongs in a page.
Locally, just announce seeds a four-hour test notice.
landingpage/examples/ holds short, complete programs that do something
useful with the data in the hub, and scripts/build_examples.py turns them
into pages. Each script runs on its own against the public endpoint with
nothing but examples/requirements.txt, which is the property to protect:
nothing in there may depend on being inside a documentation build.
Execution and rendering are deliberately separate, for the same reason the shared pages are vendored rather than fetched.
python scripts/build_examples.py --render --out .build/data # offline, every docs build
just site::examples # run the examples, refresh assets/examples/
just site::examples 03 # just one
just site::examples-check # which figures no longer match their example
The figures in landingpage/assets/examples/ are committed. The docs build
therefore never touches S3, a changed plot arrives as a reviewable diff, and
a bucket being migrated shows up as a red gallery run rather than as a
documentation build nobody can merge past. A page whose figure is missing
still builds, with a notice in place of the plot.
Adding an example means adding a line to the nav: block in mkdocs.yml.
The strict build says so clearly if you forget.
landingpage/scripts/refresh_buckets.py regenerates
assets/waterpark-datasets.json from the live S3 endpoint, keeping any
hand-written descriptions. Needs WATERPARK_S3_KEY and
WATERPARK_S3_SECRET.
Six workflows, all with workflow_dispatch so they can be run by hand
from the Actions tab.
| workflow | runs on | what it does |
|---|---|---|
docs |
changes under landingpage/ |
strict mkdocs build; publishes to gh-pages from main |
smoke |
changes to either, or to dev/ |
brings up the compose stack and runs the stack-marked tests |
catalogue |
changed dataset configs, and daily | validates them, crawls S3, publishes a complete snapshot, and files one issue per broken dataset |
sync-shared |
weekly, or on demand | re-pins the shared pages and opens a pull request |
gallery |
changes under landingpage/examples/, weekly |
runs the examples against S3 and opens a pull request with the refreshed figures |
docs takes a deploy input, on by default, so a manual run can build
without republishing.
The dev stack above is for development only. Production is Ansible plus Podman Quadlet with nginx in front.
The docs deploy from CI to gh-pages, which nginx serves from a shallow
clone.