8.5 KiB
Local development
How to run the site on your own machine and how the tests run. The app is one
container (eb-blazor) that reads its content from a second container running
PocketBase (eb-pocketbase); both come up together as a compose stack.
For deploying a built image to Unraid, see deployment.md. For the architecture
reasoning, see architektur.md.
The three compose files
There is one stack, described by a base file plus two overlays. You never edit the base for a local run — you layer an overlay on top of it.
| File | Role |
|---|---|
compose.yaml |
Base, production-shaped: both services, no host ports, PocketBase data on the Unraid bind mount. Also holds the fixed project name eb-stack. |
compose.dev.yaml |
Dev overlay: adds host ports (5000, 8090), builds the web image from local source instead of pulling it, swaps the Unraid bind mount for a throwaway volume, seeds a local PocketBase superuser, and mounts pb/pb_migrations so a fresh volume is auto-seeded (see below). |
compose.test.yaml |
Test overlay: used only by the integration tests. Drops the fixed host port and container names so a test run does not collide with a running dev stack, and removes the seed mount so the dev seed does not run during tests. |
Project name.
name: eb-stackis set once, in the base. The overlays deliberately don't set their ownname:— a competingname:across-ffiles resolves depending on load order (and the CLI and Rider load them in opposite orders), so keeping it in one place avoids surprises. The tests override it with-p eb-test-stack.
Running the dev stack
From Rider (one click)
The run configuration DevEnvironment (.run/DevEnvironment.run.xml, checked
in) starts the stack with --build and force-recreate. It loads the dev overlay
on top of the base for you — just press run.
From the terminal
docker compose -f compose.yaml -f compose.dev.yaml up --build
PocketBase starts first; once its healthcheck passes, the web app starts
(depends_on: service_healthy). Then:
- Web app: http://localhost:5000
- PocketBase admin UI: http://localhost:8090/_/
- PocketBase health: http://localhost:8090/api/health
The dev overlay auto-creates a PocketBase superuser (
test@example.com/test-password) on a fresh volume, so you skip the/_/setup screen. These are throwaway local credentials, not the production ones.
Stop and clean up (removes the throwaway PocketBase volume too):
docker compose -f compose.yaml -f compose.dev.yaml down --volumes
If the admin UI already existed once and you want the superuser recreated from
scratch, remove the volume first: docker volume rm eb-stack_pb_data_dev.
Running the tests
dotnet test
The integration tests (PocketBaseFixture in Elternbeirat.Web.Tests) start
their own PocketBase from the very same compose files — the fixture shells
out to:
docker compose -p eb-test-stack -f compose.yaml -f compose.dev.yaml -f compose.test.yaml up --detach --wait eb-pocketbase
so the image version, environment and port are defined in one place, not
duplicated in the test code. Only eb-pocketbase is started; the web app is not
built for the tests.
Because the test stack has its own project name (eb-test-stack), auto-named
containers and a random host port, it runs happily alongside a running
dev stack — you can have DevEnvironment up in Rider and run dotnet test at the
same time. The fixture cleans up before and after each run, so a hard-killed
earlier run leaves nothing behind.
The fixture creates the collections by importing the same schema file the dev
seed uses (see the next section), so the schema is defined once. Its records,
though, are fixed in PocketBaseFixture on purpose — the tests assert against
values that file controls (a non-public draft page that must never show, an event
at a known time), not against the dev seed's content. The test overlay therefore
drops the pb_migrations mount so the dev seed does not run during tests.
Requirements: Docker with the Compose plugin must be available when the tests
run (they call docker compose).
Example content on a fresh volume (the dev seed)
A fresh dev volume comes up populated, so a first docker compose … up shows a
working site instead of an empty one. Two files under pb/pb_migrations/ do this,
and the dev overlay mounts that folder into PocketBase (./pb/pb_migrations:/pb_migrations):
| File | Role |
|---|---|
collections_schema.json |
The five content collections (pages, posts, events, faq_topics, faqs) as PocketBase exports them. The single source of truth for the schema — the test fixture imports this same file. |
<timestamp>_dev_seed.js |
The PocketBase migration. Reads the schema file, creates any missing collection, then inserts the example records. |
Both steps are idempotent: a collection is created only if it is missing, and filled only while it is empty. So a populated volume — or the production DB, should this ever run there — is never touched. To start over from an empty, then freshly seeded volume:
docker volume rm eb-stack_pb_data_dev
docker compose -f compose.yaml -f compose.dev.yaml up --build
Seed only in dev. The
pb_migrationsmount lives incompose.dev.yamlalone. The base (production) file does not mount it, and the test overlay removes it again — production content is seeded by hand, never from this repo.
Refreshing the seed after a schema or content change
The seed is generated from a running dev instance, not hand-edited. When you change a collection's schema in the admin UI, or want to capture the current example content as the new seed, re-export from the running dev PocketBase (authenticated as the dev superuser) and regenerate the two files. The schema file is the plain export of the four collections; the migration embeds the records the same way. After regenerating, prove it end to end:
docker volume rm eb-stack_pb_data_dev # force a fresh volume
docker compose -f compose.yaml -f compose.dev.yaml up --build
dotnet test # fixture imports the new schema
Content while developing
The app reads its content from PocketBase over the compose network
(PocketBase__BaseUrl=http://eb-pocketbase:8090 — the service name, resolved
inside the compose network, independent of the container name). Editing content
means editing records in the PocketBase admin UI at
http://localhost:8090/_/, not editing files. See redaktion.md.
Adding a new content type
Editors can add records to the existing collections and add plain text
pages, but a new kind of list — its own collection rendered on its own
route with its own sorting or grouping — is a development task, not editorial.
It takes three steps, all small because the plumbing is shared:
- Collection + DTO. Create the collection in the PocketBase admin (give it a
publicbool and open List/View rules, like the others), then add a matching record type inElternbeirat.Contractswith[JsonPropertyName(...)]on each field — mirrorPage/Post. - Client method. Add one method to
PocketBaseClient. The sharedGetRecordsAsync<T>(collection, sort, ct)already does thepublic=truefilter, the sort and the deserialization, so a new type is a one-liner:public Task<IReadOnlyList<Minutes>> GetMinutesAsync(CancellationToken ct = default)=> GetRecordsAsync<Minutes>("minutes", "-date", ct); - Feature component. Add a component under
Features/<Name>/with its own@page "/…"route that injectsPocketBaseClient, calls the new method and renders the result. Literal routes win over the/{Slug}catch-all, so pick a slug that nopagesrecord needs. Follow an existing list (PostList,EventList,FaqList) for the load-in-OnInitializedAsync, log-and-degrade pattern, and add a route smoke test plus a fixture seed.
Also refresh the seed so the new collection comes up on a fresh volume and in the
tests: re-export it into pb/pb_migrations/collections_schema.json (the shared
schema) and add example rows to the dev seed migration — see Refreshing the seed
above. The test fixture picks the new schema up automatically once it is in the
file.
Text pages need none of this — they are all served generically by
ContentPage (@page "/{Slug}"), which is why a new pages record is live
without a rebuild.