From f7fad64bcba78a7a7ec8f51456b22ded808a8c15 Mon Sep 17 00:00:00 2001 From: tleininger Date: Tue, 22 Sep 2026 13:39:27 +0200 Subject: [PATCH] Dissolve plan.md into docs/, move open work to Gitea issues --- .env.example | 11 + .gitignore | 5 +- CLAUDE.md | 18 +- Elternbeirat.Web/Content/pages/datenschutz.md | 2 +- Elternbeirat.Web/Content/pages/impressum.md | 2 +- docs/architektur.md | 72 +++ docs/deployment.md | 31 +- docs/recht.md | 36 ++ plan.md | 473 ------------------ 9 files changed, 142 insertions(+), 508 deletions(-) create mode 100644 .env.example create mode 100644 docs/architektur.md create mode 100644 docs/recht.md delete mode 100644 plan.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bcd4377 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Template for the local .env file (which is gitignored and holds real secrets). +# Copy this to .env and fill in the real values. Never commit .env. + +# Gitea access token with scope "issue: Read and Write", used to read and create +# issues on the Gitea instance. Create it at: +# Gitea -> Settings -> Applications -> Access Tokens (name "issue-tracking") +GITEA_TOKEN= + +# Base URL of the Gitea instance and the repo path issues belong to. +GITEA_URL=https://gitea.anticarnist.de +GITEA_REPO=Tom/Elternbeirat diff --git a/.gitignore b/.gitignore index fcf70a4..1e06d8f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ riderModule.iml # Altbestand der WordPress-Seite (Sichtung/Migration, kann DB-Dumps mit # personenbezogenen Daten und grosse Binaerdateien enthalten) -- nie ins Repo. -/backup/ \ No newline at end of file +/backup/ + +# Secrets (Gitea-Token o.ae.) -- niemals ins Repo. Vorlage: .env.example +.env \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 6de3178..4ef71d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,23 +4,28 @@ Website des Elternbeirats der IGMH. Blazor Web App mit statischem Server-Side-Rendering, .NET 10. Läuft als Container auf Unraid hinter dem Nginx Proxy Manager. -Architekturentscheidungen und deren Begründung: `plan.md`. Dort nachlesen, bevor -eine davon in Frage gestellt wird. +Architekturentscheidungen und deren Begründung: `docs/architektur.md`. Dort +nachlesen, bevor eine davon in Frage gestellt wird. ## Wann welches Dokument | Thema | Datei | |---|---| -| Warum SSR statt WASM, warum keine DB, offene Punkte, Meilensteine | `plan.md` | +| Warum SSR statt WASM, warum keine DB (AE-1 bis AE-4) | `docs/architektur.md` | | Unraid, compose, NPM-Proxy-Host, Registry, Rollback | `docs/deployment.md` | -| Strato, DynDNS, Mail-Records | `docs/dns.md` | | Inhalte anlegen und ändern | `docs/inhalte-pflegen.md` | | Impressum, Datenschutz, Fotos | `docs/recht.md` | +| Offene Arbeit, Meilensteine, offene Punkte | Gitea-Issues (Milestone „Elternbeirat-Website") | + +Offene Aufgaben liegen als **Gitea-Issues**, nicht als Markdown im Repo. So löst +das Anlegen oder Ändern eines Tickets keinen Build aus. Die vier Arbeitsstränge +(Redaktionssystem, Deployment, Inhalte, Infrastruktur) sind Parent-Issues mit +Sub-Issues als Checkliste im Body, alle am Milestone „Elternbeirat-Website". ## Kommandos ```bash -dotnet run --project Elternbeirat.Web # lokal, Content-Reload per FileSystemWatcher +dotnet run --project Elternbeirat.Web # lokal; Inhalte werden beim Start gelesen (kein Auto-Reload) dotnet test # Smoke-Tests: jede Route liefert 200 docker compose build && docker compose up -d ``` @@ -31,7 +36,8 @@ vollständige Stand. ## Nicht offensichtlich - **Inhalte liegen im Image**, nicht in einem Volume. Jede Textänderung braucht - Commit und Rebuild. Das ist Absicht (`plan.md`, AE-3) — nicht „vereinfachen". + Commit und Rebuild. Das ist Absicht (`docs/architektur.md`, AE-3) — nicht + „vereinfachen". - **Kein `UseHttpsRedirection()`, kein `UseHsts()`.** NPM terminiert TLS; beides erzeugt hinter dem Proxy eine Redirect-Schleife. `UseForwardedHeaders` mit geleerten `KnownNetworks`/`KnownProxies` ist korrekt so, weil der Container kein diff --git a/Elternbeirat.Web/Content/pages/datenschutz.md b/Elternbeirat.Web/Content/pages/datenschutz.md index b64b662..813951e 100644 --- a/Elternbeirat.Web/Content/pages/datenschutz.md +++ b/Elternbeirat.Web/Content/pages/datenschutz.md @@ -5,7 +5,7 @@ title: Datenschutz # Datenschutzerklärung *TODO: Rechtlich verbindliche Datenschutzerklärung ergänzen. Erst nach Freigabe -finalisieren – siehe `docs/recht.md` (noch anzulegen).* +finalisieren – siehe `docs/recht.md`.* Diese Website wird bewusst ohne externe Ressourcen betrieben: keine CDN-Skripte, keine Google Fonts, keine Karten- oder Video-Einbettungen. Schriften werden von diff --git a/Elternbeirat.Web/Content/pages/impressum.md b/Elternbeirat.Web/Content/pages/impressum.md index 26b3b1e..3de2511 100644 --- a/Elternbeirat.Web/Content/pages/impressum.md +++ b/Elternbeirat.Web/Content/pages/impressum.md @@ -5,7 +5,7 @@ title: Impressum # Impressum *TODO: Rechtlich verbindliches Impressum ergänzen. Erst nach Freigabe mit echten -Daten füllen – siehe `docs/recht.md` (noch anzulegen).* +Daten füllen – siehe `docs/recht.md`.* ## Angaben gemäß § 5 DDG diff --git a/docs/architektur.md b/docs/architektur.md new file mode 100644 index 0000000..a6f481c --- /dev/null +++ b/docs/architektur.md @@ -0,0 +1,72 @@ +# Architecture decisions + +Why the site is built the way it is. Read this before questioning one of these +decisions — each records the reasoning, not just the choice. Operational how-to +lives in `docs/deployment.md`; open work lives in `tasks/`. + +> **Note on scope.** These decisions describe stage 1: a static information site +> with content as files. The move to editor-based content maintenance +> (`tasks/001-redaktion-ohne-entwickler.md`) deliberately revisits AE-2 and AE-3 — +> when that lands, update this file. + +## AE-1: Blazor with static server-side rendering, not WebAssembly + +**Decision:** Blazor Web App with interactivity mode *None* (pure static SSR). +Target framework .NET 10 (LTS). + +**Rationale:** Blazor WASM loads several MB of runtime before the first visible +letter, serves search engines and link previews (WhatsApp, Signal, Messenger — the +main distribution channel among parents) an empty shell, and provides no value on a +pure information site. Static SSR delivers finished HTML, needs no JavaScript, and +costs around 60 MB of RAM in the container. + +**Consequence:** Interactivity can be added later per component +(`@rendermode InteractiveServer` on exactly that one component), without changing the +architecture. Never global. + +## AE-2: Content as files in the repo, not in a database + +**Decision:** Page content as Markdown with YAML frontmatter, events as structured +YAML (`Content/events.yml`), documents as PDF under `wwwroot`. + +**Rationale:** No DB backup, no migration schema, no consistency problems between +files and database. Changes are commits and are therefore traceable and reversible. +For a site with ~10 subpages and a few events per year, anything else is overhead. + +**Consequence:** Text changes require a commit and a redeploy. With an expected five +changes a year that is acceptable — and the reason the editor expansion +(`tasks/001`) is treated as a separate stage. + +## AE-3: Content is baked into the image, not mounted as a volume + +**Decision:** `Content/` and `wwwroot/` are part of the image. + +**Rationale:** Mounted content from the appdata share could be edited directly on the +NAS, but that is exactly when the repo and the live state drift apart — and the +"backup = `git clone`" property from AE-2 would be worthless. + +**Consequence:** No quick fix on the live system. Typos are corrected properly via a +commit. Note: the content is read once at startup and cached — there is **no** +`FileSystemWatcher` (a change needs a restart / `dotnet watch`). + +## AE-4: TLS and certificates exclusively in the Nginx Proxy Manager + +**Decision:** The container speaks only HTTP on port 8080 and has **no** port mapping +to the outside. NPM terminates TLS and is the only path to the container. + +**Rationale:** Matches the pattern already established in the home lab +(`cloud.anticarnist.de`). Certificate management stays in one place. + +**Consequence (important):** In `Program.cs`, **no** `UseHttpsRedirection()` and +**no** `UseHsts()` — otherwise a redirect loop behind the proxy. NPM sets HSTS. +`UseForwardedHeaders` with emptied `KnownNetworks`/`KnownProxies` is correct because +the container has no port mapping and is reachable only through NPM. + +## Rejected alternatives (for AE-1) + +| Alternative | Why not | +|---|---| +| Blazor WASM | Payload, SEO, link previews, no benefit | +| ASP.NET Core MVC/Razor Pages | Works too, but Razor Components are the more modern model | +| Statiq.Web (C# SSG) → nginx | Leanest to operate, but every text change forces a build. Kept as a fallback. | +| Astro/Hugo | More mature SSG ecosystem, but unfamiliar territory | diff --git a/docs/deployment.md b/docs/deployment.md index e2d9a29..805c893 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -187,33 +187,12 @@ Requires two repository secrets (Gitea → repo → **Settings** → **Actions** - `REGISTRY_TOKEN` — a Gitea access token with **`package: write`**, *not* the login password. +The workflow currently only builds and pushes — it does **not** run the tests +yet, and it does **not** deploy. Those open steps (tests in the workflow, +auto-deploy with a health gate via Watchtower, encrypting the registry token on +Unraid) are tracked in `tasks/002-deployment-automatisieren.md`. + Two known pitfalls: the `act_runner` needs Docker socket access to build, and it must offer the `ubuntu-latest` label the workflow asks for. To check the runner: Gitea → repo (or site admin) → **Settings** → **Actions** → **Runners** — it should be listed as **online** with a label set that includes `ubuntu-latest`. - -### Auto-deploy via Watchtower (open — needs a health gate first) - -Automatically rolling out `:latest` the moment it lands in the registry is -tempting, but **not** something to switch on blindly: a build can be green and -still serve a broken page (a bad content file, a runtime-only culture crash like -the de-DE one). Auto-deploy without a gate would push that live **unnoticed**. - -So before turning this on, decide the gate: - -- The container must prove itself **healthy** before it replaces the running one - — but the chiseled image has no shell, so a `HEALTHCHECK` with `curl`/`sh` does - not work inside it. The check has to come from outside (e.g. an external probe - hitting a known route, or a compose-level check from a sidecar). -- Watchtower must be **label-scoped** to *only* the `eb-web` container, otherwise - it updates the entire home-lab inventory. -- Keep a fast **rollback**: pin `:` in the Unraid compose and bring it back - up (see "Rollback" above). - -Until that gate exists, deployment stays manual on purpose. - -### Encrypt the registry token on Unraid (open) - -Currently the token sits in plain text in `/root/.docker/config.json`. Set up a -credential helper later, so that the plain-text warning from `docker login` -disappears. diff --git a/docs/recht.md b/docs/recht.md new file mode 100644 index 0000000..bc0efac --- /dev/null +++ b/docs/recht.md @@ -0,0 +1,36 @@ +# Legal — imprint, privacy, photos + +Not legal advice — but the points where school sites regularly get flagged. The +actual imprint and privacy texts live in `Content/pages/impressum.md` and +`Content/pages/datenschutz.md`; filling them in with released, real data is tracked +in `tasks/003-echte-inhalte-vor-go-live.md`. + +## Imprint (§ 5 DDG) + +If the Elternbeirat has no legal form of its own, the operator is listed personally +in the imprint with name and a valid postal address for service. This is a decision, +not a formality — the private address becomes public. Alternative: the school's +address, but only with its explicit consent and if the school is a co-operator. + +## Privacy policy + +With the move, Tom becomes the controller in the sense of the GDPR. Name the server +logs with IP addresses, define the legal basis and the deletion period. + +## No external resources + +Google Fonts, Maps, YouTube embeds, and CDN scripts transmit visitors' IPs to third +parties. Fonts are served by ourselves. This is the reason behind the "no external +resources" rule in `CLAUDE.md` — data protection, not taste. + +## Photos of children + +Only with the consent of the legal guardians — by far the most common mistake on +school sites. When in doubt, no photos of people. Note: the old WordPress backup +contained no own images anyway (only URLs in the database), so any photo used is a +fresh, consciously released one. + +## Hosting on a private connection + +The public IP of the private connection appears in the DNS of a school site. A +deliberate decision, not a side effect. diff --git a/plan.md b/plan.md deleted file mode 100644 index 73d24ce..0000000 --- a/plan.md +++ /dev/null @@ -1,473 +0,0 @@ -# elternbeirat-igmh.de — Rebuild - -Replacing the previous WordPress site with a custom .NET application on -self-hosted infrastructure. - -As of: 2026-09-20 · Responsible: Tom (Thomas Leininger) - ---- - -## 1. Starting Situation - -- The domain `elternbeirat-igmh.de` was held by Maik Palm (departing - Elternbeirat member) under the "STRATO Hosting Basic" package, order number 9157927. -- The change of domain owner and the domain transfer are signed by both parties - (19.09.2026); the submission to Strato is still pending. -- Target package: Tom's "STRATO Mail Plus" (order number 8844576) — domain + email, - **no web space**. -- **Email stays with Strato.** Only the website moves to self-hosted hardware. -- **The previous website is currently offline.** The content is therefore the - most time-critical open item (→ section 9). - ---- - -## 2. Goals and Non-Goals - -**Goals** - -- A public information site for the Elternbeirat: who, when, which Protokolle, - how to reach us. -- Operation on self-hosted infrastructure (Unraid), without a third-party hoster. -- Content that is versionable and needs no database — a `git clone` is the complete - backup. -- Low maintenance: no plugin updates, no PHP security holes, no CMS login as an - attack surface. - -**Non-Goals (deliberate)** - -- No CMS with a web editor in stage 1. If other Elternbeirat members should later - edit content themselves, that is a separate expansion step (→ section 11). -- No user accounts, no login, no members' area. -- No database. -- No external integrations (fonts, analytics, maps, social widgets) — for - data-protection reasons, see section 10. - ---- - -## 3. Architecture Decisions - -### AE-1: Blazor with Static Server-Side Rendering, not WebAssembly - -**Decision:** Blazor Web App with interactivity mode *None* (pure static SSR). -Target framework .NET 10 (LTS). - -**Rationale:** Blazor WASM loads several MB of runtime before the first visible -letter, serves search engines and link previews (WhatsApp, Signal, Messenger — -the main distribution channel among parents) an empty shell, and provides no value -whatsoever on a pure information site. Static SSR delivers finished HTML, needs no -JavaScript, and costs around 60 MB of RAM in the container. - -**Consequence:** Interactivity can be added later per component -(`@rendermode InteractiveServer` on exactly that one component), without changing -the architecture. - -**Rejected alternatives:** - -| Alternative | Why not | -|---|---| -| Blazor WASM | Payload, SEO, link previews, no benefit | -| ASP.NET Core MVC/Razor Pages | Works just as well, but Razor Components are the more modern model | -| Statiq.Web (C# SSG) → nginx | Leanest from an ops standpoint (nothing to patch), but every text change forces a build run. Kept as a fallback. | -| Astro/Hugo | More mature SSG ecosystem, but unfamiliar territory | - -### AE-2: Content as files in the repo, not in a database - -**Decision:** Page content as Markdown with YAML frontmatter, Termine as -structured YAML, documents (Protokolle, bylaws) as PDF under `wwwroot`. - -**Rationale:** No DB backup, no migration schema, no consistency problems between -files and database. Changes are commits and are therefore traceable and -reversible. For a site with ~10 subpages and a few Termine per year, anything else -is overhead. - -**Consequence:** Text changes require a commit and a redeploy. With an expected -five changes a year that is acceptable — and the reason why section 11 treats the -expansion to a web editor as a separate stage. - -### AE-3: Content is baked into the image, not mounted as a volume - -**Decision:** `Content/` and `wwwroot/` are part of the image. - -**Rationale:** Mounted content from the appdata share could be edited directly on -the NAS, but that is exactly when the repo and the live state drift apart — and -the "backup = `git clone`" property from AE-2 would be worthless. - -**Consequence:** No quick fix on the live system. Typos are corrected properly via -a commit. - -### AE-4: TLS and certificates exclusively in the Nginx Proxy Manager - -**Decision:** The container speaks only HTTP on port 8080 and has **no** port -mapping to the outside. NPM terminates TLS and is the only path to the container. - -**Rationale:** Matches the pattern already established in the home lab -(`cloud.anticarnist.de`). Certificate management stays in one place. - -**Consequence (important):** In `Program.cs`, **no** `UseHttpsRedirection()` and -**no** `UseHsts()` — otherwise a redirect loop behind the proxy. NPM sets HSTS. - ---- - -## 4. Creating the Project (Rider) - -Template **Blazor Web App** with these options: - -| Option | Value | -|---|---| -| Framework | .NET 10.0 | -| Authentication | None | -| Interactive render mode | **None** | -| Include sample pages | off | -| Configure for HTTPS | on (only relevant for local development) | -| Do not use top-level statements | doesn't matter | -| Enlist in .NET Aspire orchestration | **off** | - -Project name: `Elternbeirat.Web`, Solution: `Elternbeirat`. - -The only thing that matters is *Interactive render mode = None* — this way the -template creates no `.Client` project and no WebAssembly bundle. - -**Created and confirmed (2026-09-20):** Blazor Web App, `net10.0`, Interactive -render mode `None`, Auth `None`, sample pages off, Docker options in the dialog off -(we write the Dockerfile ourselves, see section 7). Git repository created along -with it. The solution sits directly under `RiderProjects\Elternbeirat\`, the -project in `Elternbeirat.Web\` below it — **no** intermediate `src/` directory, -unlike originally sketched in section 5. - -NuGet packages that get added: - -- `Markdig` — Markdown rendering -- `YamlDotNet` — frontmatter and `termine.yml` - ---- - -## 5. Repo Structure - -``` -Elternbeirat/ ← repo root, = solution directory -├── Elternbeirat.sln -├── plan.md ← this document -├── CLAUDE.md ← minimal: triggers, non-obvious commands -├── docs/ -│ ├── deployment.md ← Unraid, NPM, registry, rollback -│ ├── dns.md ← Strato, DynDNS, mail records -│ ├── inhalte-pflegen.md ← guide for the not-everyday case -│ ├── inhalte-migration.md ← import from the old WordPress -│ └── recht.md ← imprint, privacy, photos -├── Elternbeirat.Web/ -│ ├── Elternbeirat.Web.csproj -│ ├── Components/ -│ │ ├── App.razor -│ │ ├── Routes.razor -│ │ ├── Layout/ MainLayout, NavMenu, Footer -│ │ └── Pages/ Start, UeberUns, Termine, Protokolle, -│ │ News, NewsBeitrag, Kontakt, -│ │ Impressum, Datenschutz, Fehler404 -│ ├── Content/ ← content, no code -│ │ ├── seiten/*.md -│ │ ├── news/2026-09-20-titel.md -│ │ └── termine.yml -│ ├── Services/ -│ │ ├── MarkdownContentService.cs -│ │ ├── TermineService.cs -│ │ └── IcsWriter.cs -│ ├── wwwroot/ -│ │ ├── css/site.css ← own CSS, no CDN integration -│ │ ├── img/ -│ │ └── dokumente/ ← Protokolle, bylaws (PDF) -│ └── Program.cs -├── Elternbeirat.Web.Tests/ ← smoke tests: every route returns 200 -├── Dockerfile -├── compose.yaml -├── .dockerignore -└── .gitea/workflows/deploy.yml -``` - -`CLAUDE.md` deliberately stays short (build/run commands, style rules, pointer to -`docs/`). The details live in `docs/` and are read only when needed. - ---- - -## 6. Content Model - -**Page** (`Content/seiten/ueber-uns.md`): - -```markdown ---- -titel: Über uns -slug: ueber-uns -reihenfolge: 20 -beschreibung: Wer im Elternbeirat der IGMH mitarbeitet und wie wir arbeiten. ---- - -## Der Elternbeirat - -Body text … -``` - -**Termin** (`Content/termine.yml`): - -```yaml -- titel: Elternbeiratssitzung - beginn: 2026-10-14T19:30:00 - ende: 2026-10-14T21:00:00 - ort: IGMH, Raum A103 - oeffentlich: true - notiz: Gäste willkommen -``` - -**News post** (`Content/news/2026-09-20-neue-website.md`) — like a page, plus -`datum` and `autor`. - -The services read everything at startup, cache it in memory, and provide it in a -typed form. In Development there is also a `FileSystemWatcher`, so that text -changes become visible without a restart. - -**Added benefit at no extra cost:** an ICS endpoint at `/termine.ics` that serves -the public Termine. Parents subscribe to the calendar once on their phone and see -every session automatically. This is the one point where the self-built solution -noticeably beats the old WordPress site. - ---- - -## 7. Build and Deployment - -### Stage 1 — Manual (start here) - -```bash -docker compose build -docker compose up -d -``` - -Entirely sufficient for five deployments a year. Building via CI in advance would -be an end in itself. - -### Stage 2 — Gitea Actions (runner is available) - -`.gitea/workflows/deploy.yml` on push to `main`: - -1. `actions/checkout` -2. Login to Gitea's own container registry -3. `docker/build-push-action` → `gitea./tom/elternbeirat:latest` **and** `:${{ gitea.sha }}` - -Two pitfalls that experience shows cost time: - -- The `act_runner` in Docker mode needs access to a Docker socket or a DinD - service, otherwise `build-push-action` fails. -- The Gitea registry needs a package token with write permission (`write:package`), - not the normal login password. - -**Always push the SHA tag too.** `:latest` alone makes rollback impossible. - -### Redeploy on Unraid - -Watchtower, but **label-scoped** — otherwise it updates the entire home-lab -inventory unasked: - -```yaml -# in the Watchtower container -WATCHTOWER_LABEL_ENABLE: "true" -``` - -```yaml -# in the eb-web service -labels: - com.centurylinklabs.watchtower.enable: "true" -``` - -Alternatively: press "Update" manually in the Unraid Docker tab. At this rate of -change entirely legitimate. - -### Dockerfile (sketch) - -```dockerfile -FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build -WORKDIR /src -COPY Elternbeirat.Web/Elternbeirat.Web.csproj Elternbeirat.Web/ -RUN dotnet restore Elternbeirat.Web/Elternbeirat.Web.csproj -COPY . . -RUN dotnet publish Elternbeirat.Web/Elternbeirat.Web.csproj -c Release -o /app - -FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled AS runtime -WORKDIR /app -COPY --from=build /app . -EXPOSE 8080 -ENTRYPOINT ["dotnet", "Elternbeirat.Web.dll"] -``` - -The chiseled image runs as non-root (UID 1654) by default from .NET 8 on and -listens on port 8080. **It contains no shell** — a `HEALTHCHECK` with `curl` does -not work there. Either use the normal `aspnet:10.0-noble` or leave monitoring to -NPM or Uptime Kuma. - ---- - -## 8. Hosting on Unraid - -```yaml -# compose.yaml -services: - eb-web: - image: gitea./tom/elternbeirat:latest - container_name: eb-web - restart: unless-stopped - environment: - ASPNETCORE_URLS: http://+:8080 - TZ: Europe/Berlin - networks: [npm] - labels: - com.centurylinklabs.watchtower.enable: "true" - -networks: - npm: - external: true -``` - -No `ports:` block. The container is reachable exclusively via the NPM Docker -network. - -**NPM Proxy Host:** - -| Field | Value | -|---|---| -| Domain Names | `elternbeirat-igmh.de`, `www.elternbeirat-igmh.de` | -| Scheme | `http` | -| Forward Hostname | `eb-web` | -| Forward Port | `8080` | -| Block Common Exploits | on | -| Websockets Support | off (not needed with static SSR) | -| SSL | Let's Encrypt, Force SSL, HTTP/2, HSTS | - -**Don't forget in `Program.cs`:** - -```csharp -app.UseForwardedHeaders(new ForwardedHeadersOptions -{ - ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto, - // NPM ist der einzige Weg zum Container (kein Port-Mapping), - // daher ist das Leeren der Allowlists hier vertretbar: - KnownNetworks = { }, KnownProxies = { } -}); -``` - -Without this, the app sees every request as HTTP and with the proxy IP instead of -the client IP — relevant for correct absolute URLs and for the logs. - ---- - -## 9. Content — time-critical - -The old site is offline, but Maik's Strato contract is still running. As long as it -runs, the web space is reachable; after cancellation the content is gone for good. - -**Order of rescue attempts:** - -1. Check what was actually secured at the meeting with Maik (files? MySQL dump? - both?). A complete dump would be the ideal case — from it, texts, page - structure, media, and PDFs can be extracted cleanly. -2. If only files are available: `wp-content/uploads` contains images and PDFs, but - the texts live in the database. Then step 3. -3. Check the Wayback Machine for snapshots of `elternbeirat-igmh.de`. -4. If none of that works: ask Maik to pull an export before the contract ends — or - rebuild the content from the Protokolle and the school's website. - -**This question blocks the content part, not the technical part.** The scaffolding -can be built with placeholders and filled in later. - ---- - -## 10. Legal - -Not legal advice — but the points where school sites regularly get flagged: - -- **Imprint (§ 5 DDG):** If the Elternbeirat has no legal form of its own, the - operator is listed personally in the imprint with name and a valid postal - address for service. This is a decision, not a formality — the private address - becomes public. Alternative: the school's address, but only with its explicit - consent and if the school is a co-operator. -- **Privacy policy:** With the move, Tom becomes the controller in the sense of the - GDPR. Name server logs with IP addresses, define the legal basis and the deletion - period. -- **No external resources.** Google Fonts, Maps, YouTube embeds, and CDN scripts - transmit visitors' IPs to third parties. Fonts are served by ourselves. -- **Photos of children:** only with the consent of the legal guardians — by far the - most common mistake on school sites. When in doubt, no photos of people. -- Hosting on a private connection means: the public IP of the private connection - appears in the DNS of a school site. A deliberate decision, not a side effect. - ---- - -## 11. Open Items - -| # | Item | Status | -|---|---|---| -| 1 | What was secured from the old WordPress? | **open, time-critical** | -| 2 | Can "STRATO Mail Plus" do DynDNS? Per the Strato FAQ from "PowerWeb Basic 2013 or STRATO Domain" on — whether Mail Plus counts is unclear. Ask support along with the transfer while it is in progress. | open | -| 3 | DynDNS updater: Fritzbox (which, as an exposed-host upstream device, knows the public IP) or a ddclient container on Unraid? | open | -| 4 | Should other Elternbeirat members be able to maintain content themselves? If yes: a separate expansion step (Decap CMS on a Git basis or a small admin UI). | open | -| 5 | Contact form wanted? Would require server interactivity and spam protection — `mailto:` is the effort-free alternative. | open | -| 6 | Who steps in when Tom is unavailable? A site that only one person can deploy is a dependency the board should be aware of. | open | -| 7 | Submit forms to Strato (signed, ready to go) | open | - ---- - -## 12. Implementation Order - -| # | Step | Depends on | -|---|---|---| -| 1 | Submit Strato forms, start the domain transfer | ✅ commissioned (2026-09-20) | -| 2 | Clarify the content situation (section 9) | — | -| 3 | Create the Blazor project in Rider ✅ (2026-09-20) → push to Gitea ✅ | — | -| 6 | Dockerfile, compose.yaml → as a container on Unraid ✅ (2026-09-20) | 3 | -| 4 | Content pipeline: Markdig, YamlDotNet, services, ICS endpoint | 3 | -| 5 | Layout, navigation, pages with placeholders | 4 | -| 7 | Add the real content | 2, 5 | -| 8 | Imprint and privacy policy | 7 | -| 9 | Switch DNS, NPM Proxy Host, Let's Encrypt | 1, 6 | -| 10 | Gitea Actions + registry + Watchtower | 6, 9 | - -Steps 1 and 2 run independently of the code and should start immediately — -step 2 is the only one where waiting does real damage. - -**Deviation from the original order:** Step 6 (deployment) was deliberately pulled -**ahead of** the content pipeline (4/5). Reason: prove the riskiest chain — -build image → Gitea registry → pull on Unraid → container runs — early, rather -than discovering it just before go-live. Details in `docs/deployment.md`. - -### As of 2026-09-20 (evening) - -Achieved: The raw "Hello world" of the Blazor app runs as a container on Unraid -(`Cube`), reachable on the LAN at `http://cube:5000`. This proves the complete -deploy chain including the private Gitea registry. - -Deliberate **test deviations** from the production target (section 8), to be -rolled back later: - -- `compose.yaml` has a port mapping `5000:8080`. In production: no mapping, only - via the external `npm` network (NPM is not testable yet, the domain moves first). -- Image tag only `:latest`, no SHA tag yet (→ step 10, rollback). -- The registry token sits on Unraid in plain text (`/root/.docker/config.json`). - A credential helper is noted as a later item in `docs/deployment.md`. - -**Next step:** Content pipeline (step 4) — Markdig + YamlDotNet, services for -pages/Termine, ICS endpoint. Open in parallel and independent of the code: -clarify the content situation (step 2, time-critical). - -### As of 2026-09-21 (morning) - -Deployment further built out and run through completely once: - -- **Branch/PR workflow** established: never directly on `main`; feature branch → - pull request in Gitea → merge. Carried out for the first time (PR #1). -- **Two build scripts** in `scripts/`: `dev-build.sh` (local dev image, no push — - dev states stay out of the registry) and `release.sh` (builds from `main`, tags - `:latest` **and** the short commit SHA, pushes both; aborts if not on `main` or - the working directory is dirty). -- **SHA tagging** is now standard → rollback possible. First release tag: - `:8643f2c`. Redeploy on Unraid (Compose Down/Up) deliberately practiced, works. -- `.gitattributes` enforces LF for `*.sh` (otherwise the shebang fails on Windows). - -Open for next time (unchanged): content pipeline (step 4) and the time-critical -content situation (step 2). Deployment automation via Gitea Actions (step 10) is -the next optional deployment expansion, but not urgent — manual operation via -`release.sh` is enough.