Files
Elternbeirat/docs/entwicklung.md
T

5.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, and seeds a local PocketBase superuser.
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.

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.

Requirements: Docker with the Compose plugin must be available when the tests run (they call docker compose).


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.

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.