187 lines
8.4 KiB
Markdown
187 lines
8.4 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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):
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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.
|