Files
Elternbeirat/docs/entwicklung.md
T

8.4 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-stack is set once, in the base. The overlays deliberately don't set their own name: — a competing name: across -f files 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:

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 four content collections (pages, posts, events, 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_migrations mount lives in compose.dev.yaml alone. 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:

  1. Collection + DTO. Create the collection in the PocketBase admin (give it a public bool and open List/View rules, like the others), then add a matching record type in Elternbeirat.Contracts with [JsonPropertyName(...)] on each field — mirror Page/Post.
  2. Client method. Add one method to PocketBaseClient. The shared GetRecordsAsync<T>(collection, sort, ct) already does the public=true filter, 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);
  3. Feature component. Add a component under Features/<Name>/ with its own @page "/…" route that injects PocketBaseClient, calls the new method and renders the result. Literal routes win over the /{Slug} catch-all, so pick a slug that no pages record 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.