Switch code, scripts and docs to English; drop plan.md references
This commit is contained in:
1 parent
76f1ad7825
commit
c4c7ca2ee4
15 files changed
+532
-540
No files matched your search
@@ -1,160 +1,159 @@
|
||||
# elternbeirat-igmh.de — Neuaufbau
|
||||
# elternbeirat-igmh.de — Rebuild
|
||||
|
||||
Ablösung der bisherigen WordPress-Seite durch eine eigene .NET-Anwendung auf
|
||||
eigener Infrastruktur.
|
||||
Replacing the previous WordPress site with a custom .NET application on
|
||||
self-hosted infrastructure.
|
||||
|
||||
Stand: 2026-09-20 · Verantwortlich: Tom (Thomas Leininger)
|
||||
As of: 2026-09-20 · Responsible: Tom (Thomas Leininger)
|
||||
|
||||
---
|
||||
|
||||
## 1. Ausgangslage
|
||||
## 1. Starting Situation
|
||||
|
||||
- Die Domain `elternbeirat-igmh.de` lag bei Maik Palm (ausscheidendes
|
||||
Elternbeirat-Mitglied) im Paket „STRATO Hosting Basic", Auftragsnummer 9157927.
|
||||
- Domaininhaber-Wechsel und Domainumzug sind beidseitig unterschrieben
|
||||
(19.09.2026), die Einreichung bei Strato steht noch aus.
|
||||
- Ziel-Paket: Toms „STRATO Mail Plus" (Auftragsnummer 8844576) — Domain + E-Mail,
|
||||
**kein Webspace**.
|
||||
- **E-Mail bleibt bei Strato.** Nur die Website zieht auf eigene Hardware.
|
||||
- **Die bisherige Website ist aktuell offline.** Der Inhaltsbestand ist damit der
|
||||
zeitkritischste offene Punkt (→ Abschnitt 9).
|
||||
- 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. Ziele und Nicht-Ziele
|
||||
## 2. Goals and Non-Goals
|
||||
|
||||
**Ziele**
|
||||
**Goals**
|
||||
|
||||
- Öffentliche Informationsseite des Elternbeirats: wer, wann, welche Protokolle,
|
||||
wie erreichbar.
|
||||
- Betrieb auf eigener Infrastruktur (Unraid), ohne fremden Hoster.
|
||||
- Inhalte versionierbar und ohne Datenbank — ein `git clone` ist das vollständige
|
||||
Backup.
|
||||
- Wartungsarm: keine Plugin-Updates, keine PHP-Sicherheitslücken, kein CMS-Login
|
||||
als Angriffsfläche.
|
||||
- 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.
|
||||
|
||||
**Nicht-Ziele (bewusst)**
|
||||
**Non-Goals (deliberate)**
|
||||
|
||||
- Kein CMS mit Web-Editor in Stufe 1. Falls andere Beiratsmitglieder später selbst
|
||||
redaktionell arbeiten sollen, ist das ein eigener Ausbauschritt (→ Abschnitt 11).
|
||||
- Keine Benutzerkonten, kein Login, kein Mitgliederbereich.
|
||||
- Keine Datenbank.
|
||||
- Keine externen Einbindungen (Fonts, Analytics, Maps, Social Widgets) — aus
|
||||
Datenschutzgründen, siehe Abschnitt 10.
|
||||
- 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. Architekturentscheidungen
|
||||
## 3. Architecture Decisions
|
||||
|
||||
### AE-1: Blazor mit Static Server-Side Rendering, nicht WebAssembly
|
||||
### AE-1: Blazor with Static Server-Side Rendering, not WebAssembly
|
||||
|
||||
**Entscheidung:** Blazor Web App mit Interaktivitätsmodus *None* (reines statisches
|
||||
SSR). Zielframework .NET 10 (LTS).
|
||||
**Decision:** Blazor Web App with interactivity mode *None* (pure static SSR).
|
||||
Target framework .NET 10 (LTS).
|
||||
|
||||
**Begründung:** Blazor WASM lädt mehrere MB Runtime vor dem ersten sichtbaren
|
||||
Buchstaben, liefert Suchmaschinen und Link-Vorschauen (WhatsApp, Signal, Messenger
|
||||
— der Hauptverbreitungsweg bei Elternschaften) eine leere Shell und bringt auf
|
||||
einer reinen Informationsseite keinerlei Gegenwert. Static SSR liefert fertiges
|
||||
HTML, braucht kein JavaScript und kostet im Container rund 60 MB RAM.
|
||||
**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.
|
||||
|
||||
**Konsequenz:** Interaktivität ist später pro Komponente nachrüstbar
|
||||
(`@rendermode InteractiveServer` an genau der einen Komponente), ohne die
|
||||
Architektur zu ändern.
|
||||
**Consequence:** Interactivity can be added later per component
|
||||
(`@rendermode InteractiveServer` on exactly that one component), without changing
|
||||
the architecture.
|
||||
|
||||
**Verworfene Alternativen:**
|
||||
**Rejected alternatives:**
|
||||
|
||||
| Alternative | Warum nicht |
|
||||
| Alternative | Why not |
|
||||
|---|---|
|
||||
| Blazor WASM | Payload, SEO, Link-Vorschauen, kein Nutzen |
|
||||
| ASP.NET Core MVC/Razor Pages | Funktioniert genauso, aber Razor Components sind das modernere Modell |
|
||||
| Statiq.Web (C#-SSG) → nginx | Ops-technisch am schlanksten (nichts zu patchen), aber jede Textänderung erzwingt einen Build-Lauf. Bleibt als Rückfallebene. |
|
||||
| Astro/Hugo | Ausgereifteres SSG-Ökosystem, aber fremdes Terrain |
|
||||
| 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: Inhalte als Dateien im Repo, nicht in einer Datenbank
|
||||
### AE-2: Content as files in the repo, not in a database
|
||||
|
||||
**Entscheidung:** Seiteninhalte als Markdown mit YAML-Frontmatter, Termine als
|
||||
strukturiertes YAML, Dokumente (Protokolle, Satzung) als PDF unter `wwwroot`.
|
||||
**Decision:** Page content as Markdown with YAML frontmatter, Termine as
|
||||
structured YAML, documents (Protokolle, bylaws) as PDF under `wwwroot`.
|
||||
|
||||
**Begründung:** Kein DB-Backup, kein Migrationsschema, keine Konsistenzprobleme
|
||||
zwischen Dateien und Datenbank. Änderungen sind Commits und damit nachvollziehbar
|
||||
und rückrollbar. Für eine Seite mit ~10 Unterseiten und ein paar Terminen pro Jahr
|
||||
ist alles andere Overhead.
|
||||
**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.
|
||||
|
||||
**Konsequenz:** Textänderungen erfordern einen Commit und ein Redeploy. Das ist bei
|
||||
erwarteten fünf Änderungen im Jahr akzeptabel — und der Grund, warum Abschnitt 11
|
||||
den Ausbau zum Web-Editor als eigene Stufe führt.
|
||||
**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: Inhalte werden ins Image gebacken, nicht als Volume gemountet
|
||||
### AE-3: Content is baked into the image, not mounted as a volume
|
||||
|
||||
**Entscheidung:** `Content/` und `wwwroot/` sind Teil des Images.
|
||||
**Decision:** `Content/` and `wwwroot/` are part of the image.
|
||||
|
||||
**Begründung:** Gemountete Inhalte aus dem Appdata-Share ließen sich zwar direkt
|
||||
am NAS editieren, aber genau dann driften Repo und Live-Stand auseinander — und
|
||||
die Eigenschaft „Backup = `git clone`" aus AE-2 wäre wertlos.
|
||||
**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.
|
||||
|
||||
**Konsequenz:** Kein Schnell-Fix am Live-System. Tippfehler werden korrekt über
|
||||
einen Commit behoben.
|
||||
**Consequence:** No quick fix on the live system. Typos are corrected properly via
|
||||
a commit.
|
||||
|
||||
### AE-4: TLS und Zertifikate ausschließlich im Nginx Proxy Manager
|
||||
### AE-4: TLS and certificates exclusively in the Nginx Proxy Manager
|
||||
|
||||
**Entscheidung:** Der Container spricht nur HTTP auf Port 8080 und hat **kein**
|
||||
Port-Mapping nach außen. NPM terminiert TLS und ist der einzige Weg zum Container.
|
||||
**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.
|
||||
|
||||
**Begründung:** Entspricht dem bereits etablierten Muster im Home-Lab
|
||||
(`cloud.anticarnist.de`). Zertifikatsverwaltung bleibt an einer Stelle.
|
||||
**Rationale:** Matches the pattern already established in the home lab
|
||||
(`cloud.anticarnist.de`). Certificate management stays in one place.
|
||||
|
||||
**Konsequenz (wichtig):** In `Program.cs` **kein** `UseHttpsRedirection()` und
|
||||
**kein** `UseHsts()` — sonst Redirect-Schleife hinter dem Proxy. HSTS setzt NPM.
|
||||
**Consequence (important):** In `Program.cs`, **no** `UseHttpsRedirection()` and
|
||||
**no** `UseHsts()` — otherwise a redirect loop behind the proxy. NPM sets HSTS.
|
||||
|
||||
---
|
||||
|
||||
## 4. Projekt anlegen (Rider)
|
||||
## 4. Creating the Project (Rider)
|
||||
|
||||
Template **Blazor Web App** mit diesen Optionen:
|
||||
Template **Blazor Web App** with these options:
|
||||
|
||||
| Option | Wert |
|
||||
| Option | Value |
|
||||
|---|---|
|
||||
| Framework | .NET 10.0 |
|
||||
| Authentication | None |
|
||||
| Interactive render mode | **None** |
|
||||
| Include sample pages | aus |
|
||||
| Configure for HTTPS | an (nur für lokale Entwicklung relevant) |
|
||||
| Do not use top-level statements | egal |
|
||||
| Enlist in .NET Aspire orchestration | **aus** |
|
||||
| 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** |
|
||||
|
||||
Projektname: `Elternbeirat.Web`, Solution: `Elternbeirat`.
|
||||
Project name: `Elternbeirat.Web`, Solution: `Elternbeirat`.
|
||||
|
||||
Wichtig ist allein *Interactive render mode = None* — damit erzeugt das Template
|
||||
kein `.Client`-Projekt und kein WebAssembly-Bundle.
|
||||
The only thing that matters is *Interactive render mode = None* — this way the
|
||||
template creates no `.Client` project and no WebAssembly bundle.
|
||||
|
||||
**Angelegt und bestätigt (2026-09-20):** Blazor Web App, `net10.0`, Interactive
|
||||
render mode `None`, Auth `None`, Sample pages aus, Docker-Optionen im Dialog aus
|
||||
(Dockerfile schreiben wir selbst, siehe Abschnitt 7). Git-Repository beim
|
||||
Anlegen mit erzeugt. Solution liegt direkt unter
|
||||
`RiderProjects\Elternbeirat\`, das Projekt in `Elternbeirat.Web\` darunter —
|
||||
**kein** `src/`-Zwischenverzeichnis, anders als ursprünglich in Abschnitt 5
|
||||
skizziert.
|
||||
**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-Pakete, die dazukommen:
|
||||
NuGet packages that get added:
|
||||
|
||||
- `Markdig` — Markdown-Rendering
|
||||
- `YamlDotNet` — Frontmatter und `termine.yml`
|
||||
- `Markdig` — Markdown rendering
|
||||
- `YamlDotNet` — frontmatter and `termine.yml`
|
||||
|
||||
---
|
||||
|
||||
## 5. Repo-Struktur
|
||||
## 5. Repo Structure
|
||||
|
||||
```
|
||||
Elternbeirat/ ← Repo-Root, = Solution-Verzeichnis
|
||||
Elternbeirat/ ← repo root, = solution directory
|
||||
├── Elternbeirat.sln
|
||||
├── plan.md ← dieses Dokument
|
||||
├── CLAUDE.md ← minimal: Trigger, nicht-offensichtliche Kommandos
|
||||
├── 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 ← Anleitung für den Nicht-Alltagsfall
|
||||
│ ├── inhalte-migration.md ← Übernahme aus dem alten WordPress
|
||||
│ └── recht.md ← Impressum, Datenschutz, Fotos
|
||||
│ ├── 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/
|
||||
@@ -164,7 +163,7 @@ Elternbeirat/ ← Repo-Root, = Solution-Verzeichnis
|
||||
│ │ └── Pages/ Start, UeberUns, Termine, Protokolle,
|
||||
│ │ News, NewsBeitrag, Kontakt,
|
||||
│ │ Impressum, Datenschutz, Fehler404
|
||||
│ ├── Content/ ← Inhalte, kein Code
|
||||
│ ├── Content/ ← content, no code
|
||||
│ │ ├── seiten/*.md
|
||||
│ │ ├── news/2026-09-20-titel.md
|
||||
│ │ └── termine.yml
|
||||
@@ -173,25 +172,25 @@ Elternbeirat/ ← Repo-Root, = Solution-Verzeichnis
|
||||
│ │ ├── TermineService.cs
|
||||
│ │ └── IcsWriter.cs
|
||||
│ ├── wwwroot/
|
||||
│ │ ├── css/site.css ← eigenes CSS, keine CDN-Einbindung
|
||||
│ │ ├── css/site.css ← own CSS, no CDN integration
|
||||
│ │ ├── img/
|
||||
│ │ └── dokumente/ ← Protokolle, Satzung (PDF)
|
||||
│ │ └── dokumente/ ← Protokolle, bylaws (PDF)
|
||||
│ └── Program.cs
|
||||
├── Elternbeirat.Web.Tests/ ← Smoke-Tests: jede Route liefert 200
|
||||
├── Elternbeirat.Web.Tests/ ← smoke tests: every route returns 200
|
||||
├── Dockerfile
|
||||
├── compose.yaml
|
||||
├── .dockerignore
|
||||
└── .gitea/workflows/deploy.yml
|
||||
```
|
||||
|
||||
`CLAUDE.md` bleibt bewusst kurz (Build-/Run-Kommandos, Stilregeln, Verweis auf
|
||||
`docs/`). Die Details liegen in `docs/` und werden nur bei Bedarf gelesen.
|
||||
`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. Inhaltsmodell
|
||||
## 6. Content Model
|
||||
|
||||
**Seite** (`Content/seiten/ueber-uns.md`):
|
||||
**Page** (`Content/seiten/ueber-uns.md`):
|
||||
|
||||
```markdown
|
||||
---
|
||||
@@ -203,7 +202,7 @@ beschreibung: Wer im Elternbeirat der IGMH mitarbeitet und wie wir arbeiten.
|
||||
|
||||
## Der Elternbeirat
|
||||
|
||||
Fließtext …
|
||||
Body text …
|
||||
```
|
||||
|
||||
**Termin** (`Content/termine.yml`):
|
||||
@@ -217,69 +216,69 @@ Fließtext …
|
||||
notiz: Gäste willkommen
|
||||
```
|
||||
|
||||
**News-Beitrag** (`Content/news/2026-09-20-neue-website.md`) — wie Seite, plus
|
||||
`datum` und `autor`.
|
||||
**News post** (`Content/news/2026-09-20-neue-website.md`) — like a page, plus
|
||||
`datum` and `autor`.
|
||||
|
||||
Die Services lesen beim Start alles ein, cachen es im Speicher und stellen es
|
||||
typisiert bereit. In Development zusätzlich ein `FileSystemWatcher`, damit
|
||||
Textänderungen ohne Neustart sichtbar werden.
|
||||
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.
|
||||
|
||||
**Zusatznutzen ohne Mehraufwand:** ein ICS-Endpoint unter `/termine.ics`, der die
|
||||
öffentlichen Termine ausliefert. Eltern abonnieren den Kalender einmal im Handy
|
||||
und sehen jede Sitzung automatisch. Das ist der eine Punkt, an dem die Eigenbau-
|
||||
Lösung die alte WordPress-Seite spürbar schlägt.
|
||||
**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 und Deployment
|
||||
## 7. Build and Deployment
|
||||
|
||||
### Stufe 1 — Handbetrieb (Start hier)
|
||||
### Stage 1 — Manual (start here)
|
||||
|
||||
```bash
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Für fünf Deployments im Jahr vollkommen ausreichend. CI vorab zu bauen wäre
|
||||
Selbstzweck.
|
||||
Entirely sufficient for five deployments a year. Building via CI in advance would
|
||||
be an end in itself.
|
||||
|
||||
### Stufe 2 — Gitea Actions (Runner ist vorhanden)
|
||||
### Stage 2 — Gitea Actions (runner is available)
|
||||
|
||||
`.gitea/workflows/deploy.yml` auf Push nach `main`:
|
||||
`.gitea/workflows/deploy.yml` on push to `main`:
|
||||
|
||||
1. `actions/checkout`
|
||||
2. Login an der Gitea-eigenen Container-Registry
|
||||
3. `docker/build-push-action` → `gitea.<domain>/tom/elternbeirat:latest` **und** `:${{ gitea.sha }}`
|
||||
2. Login to Gitea's own container registry
|
||||
3. `docker/build-push-action` → `gitea.<domain>/tom/elternbeirat:latest` **and** `:${{ gitea.sha }}`
|
||||
|
||||
Zwei Stolpersteine, die erfahrungsgemäß Zeit kosten:
|
||||
Two pitfalls that experience shows cost time:
|
||||
|
||||
- Der `act_runner` im Docker-Modus braucht Zugriff auf einen Docker-Socket oder
|
||||
einen DinD-Service, sonst schlägt `build-push-action` fehl.
|
||||
- Die Gitea-Registry braucht ein Paket-Token mit Schreibrecht (`write:package`),
|
||||
nicht das normale Login-Passwort.
|
||||
- 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.
|
||||
|
||||
**Immer auch den SHA-Tag pushen.** `:latest` allein macht Rollback unmöglich.
|
||||
**Always push the SHA tag too.** `:latest` alone makes rollback impossible.
|
||||
|
||||
### Redeploy auf Unraid
|
||||
### Redeploy on Unraid
|
||||
|
||||
Watchtower, aber **label-scoped** — sonst aktualisiert er ungefragt den ganzen
|
||||
Home-Lab-Bestand:
|
||||
Watchtower, but **label-scoped** — otherwise it updates the entire home-lab
|
||||
inventory unasked:
|
||||
|
||||
```yaml
|
||||
# im Watchtower-Container
|
||||
# in the Watchtower container
|
||||
WATCHTOWER_LABEL_ENABLE: "true"
|
||||
```
|
||||
|
||||
```yaml
|
||||
# im eb-web-Service
|
||||
# in the eb-web service
|
||||
labels:
|
||||
com.centurylinklabs.watchtower.enable: "true"
|
||||
```
|
||||
|
||||
Alternativ: manuell im Unraid-Docker-Tab „Update" drücken. Bei dieser
|
||||
Änderungsfrequenz völlig legitim.
|
||||
Alternatively: press "Update" manually in the Unraid Docker tab. At this rate of
|
||||
change entirely legitimate.
|
||||
|
||||
### Dockerfile (Skizze)
|
||||
### Dockerfile (sketch)
|
||||
|
||||
```dockerfile
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
@@ -296,14 +295,14 @@ EXPOSE 8080
|
||||
ENTRYPOINT ["dotnet", "Elternbeirat.Web.dll"]
|
||||
```
|
||||
|
||||
Das chiseled-Image läuft ab .NET 8 standardmäßig als non-root (UID 1654) und
|
||||
hört auf Port 8080. **Es enthält keine Shell** — ein `HEALTHCHECK` mit `curl`
|
||||
funktioniert dort nicht. Entweder das normale `aspnet:10.0-noble` verwenden oder
|
||||
die Überwachung NPM bzw. Uptime Kuma überlassen.
|
||||
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 auf Unraid
|
||||
## 8. Hosting on Unraid
|
||||
|
||||
```yaml
|
||||
# compose.yaml
|
||||
@@ -324,22 +323,22 @@ networks:
|
||||
external: true
|
||||
```
|
||||
|
||||
Kein `ports:`-Block. Der Container ist ausschließlich über das NPM-Docker-Netz
|
||||
erreichbar.
|
||||
No `ports:` block. The container is reachable exclusively via the NPM Docker
|
||||
network.
|
||||
|
||||
**NPM Proxy Host:**
|
||||
|
||||
| Feld | Wert |
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Domain Names | `elternbeirat-igmh.de`, `www.elternbeirat-igmh.de` |
|
||||
| Scheme | `http` |
|
||||
| Forward Hostname | `eb-web` |
|
||||
| Forward Port | `8080` |
|
||||
| Block Common Exploits | an |
|
||||
| Websockets Support | aus (wird bei statischem SSR nicht gebraucht) |
|
||||
| Block Common Exploits | on |
|
||||
| Websockets Support | off (not needed with static SSR) |
|
||||
| SSL | Let's Encrypt, Force SSL, HTTP/2, HSTS |
|
||||
|
||||
**In `Program.cs` nicht vergessen:**
|
||||
**Don't forget in `Program.cs`:**
|
||||
|
||||
```csharp
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
@@ -351,125 +350,124 @@ app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
});
|
||||
```
|
||||
|
||||
Ohne das sieht die App jede Anfrage als HTTP und mit der Proxy-IP statt der
|
||||
Client-IP — relevant für korrekte absolute URLs und für die Logs.
|
||||
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. Inhalte — zeitkritisch
|
||||
## 9. Content — time-critical
|
||||
|
||||
Die alte Seite ist offline, Maiks Strato-Vertrag läuft aber noch. Solange er läuft,
|
||||
ist der Webspace erreichbar; nach der Kündigung ist der Bestand endgültig weg.
|
||||
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.
|
||||
|
||||
**Reihenfolge der Rettungsversuche:**
|
||||
**Order of rescue attempts:**
|
||||
|
||||
1. Prüfen, was beim Termin mit Maik tatsächlich gesichert wurde (Dateien? MySQL-Dump?
|
||||
Beides?). Ein vollständiger Dump wäre der Idealfall — daraus lassen sich Texte,
|
||||
Seitenstruktur, Medien und PDFs sauber extrahieren.
|
||||
2. Falls nur Dateien vorliegen: `wp-content/uploads` enthält Bilder und PDFs, die
|
||||
Texte liegen aber in der Datenbank. Dann Schritt 3.
|
||||
3. Wayback Machine auf Snapshots von `elternbeirat-igmh.de` prüfen.
|
||||
4. Falls nichts davon greift: Maik bitten, vor Vertragsende noch einen Export zu
|
||||
ziehen — oder Inhalte aus den Protokollen und von der Schulseite neu aufbauen.
|
||||
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.
|
||||
|
||||
**Diese Frage blockiert den Inhaltsteil, nicht den Technikteil.** Das Gerüst lässt
|
||||
sich mit Platzhaltern bauen und später befüllen.
|
||||
**This question blocks the content part, not the technical part.** The scaffolding
|
||||
can be built with placeholders and filled in later.
|
||||
|
||||
---
|
||||
|
||||
## 10. Rechtliches
|
||||
## 10. Legal
|
||||
|
||||
Kein Rechtsrat — aber die Punkte, an denen Schulseiten regelmäßig auffallen:
|
||||
Not legal advice — but the points where school sites regularly get flagged:
|
||||
|
||||
- **Impressum (§ 5 DDG):** Hat der Elternbeirat keine eigene Rechtsform, steht der
|
||||
Betreiber persönlich mit Name und ladungsfähiger Anschrift im Impressum. Das ist
|
||||
eine Entscheidung, keine Formalie — die Privatadresse wird damit öffentlich.
|
||||
Alternative: Anschrift der Schule, aber nur mit deren ausdrücklichem Einverständnis
|
||||
und wenn die Schule Mitbetreiberin ist.
|
||||
- **Datenschutzerklärung:** Mit dem Umzug ist Tom Verantwortlicher im Sinne der DSGVO.
|
||||
Server-Logs mit IP-Adressen benennen, Rechtsgrundlage und Löschfrist festlegen.
|
||||
- **Keine externen Ressourcen.** Google Fonts, Maps, YouTube-Embeds und CDN-Skripte
|
||||
übertragen die IP der Besucher an Dritte. Fonts werden selbst ausgeliefert.
|
||||
- **Fotos von Kindern:** nur mit Einwilligung der Erziehungsberechtigten — bei
|
||||
Schulseiten der mit Abstand häufigste Fehler. Im Zweifel keine Personenfotos.
|
||||
- Hosting am privaten Anschluss bedeutet: Die öffentliche IP des Privatanschlusses
|
||||
steht im DNS einer Schulseite. Bewusste Entscheidung, kein Nebeneffekt.
|
||||
- **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. Offene Punkte
|
||||
## 11. Open Items
|
||||
|
||||
| # | Punkt | Status |
|
||||
| # | Item | Status |
|
||||
|---|---|---|
|
||||
| 1 | Was wurde vom alten WordPress gesichert? | **offen, zeitkritisch** |
|
||||
| 2 | Kann „STRATO Mail Plus" DynDNS? Laut Strato-FAQ ab „PowerWeb Basic 2013 bzw. STRATO Domain" — ob Mail Plus dazuzählt, ist unklar. Beim Support mit anfragen, solange der Umzugsvorgang läuft. | offen |
|
||||
| 3 | DynDNS-Updater: Fritzbox (kennt als Exposed-Host-Vorschaltgerät die öffentliche IP) oder ddclient-Container auf Unraid? | offen |
|
||||
| 4 | Sollen andere Beiratsmitglieder Inhalte selbst pflegen können? Falls ja: eigener Ausbauschritt (Decap CMS auf Git-Basis oder kleines Admin-UI). | offen |
|
||||
| 5 | Kontaktformular gewünscht? Würde Server-Interaktivität und Spam-Schutz erfordern — `mailto:` ist die aufwandsfreie Alternative. | offen |
|
||||
| 6 | Wer springt ein, wenn Tom nicht verfügbar ist? Eine Seite, die nur einer deployen kann, ist eine Abhängigkeit, die der Beirat kennen sollte. | offen |
|
||||
| 7 | Formulare bei Strato einreichen (unterschrieben, liegt bereit) | offen |
|
||||
| 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. Umsetzungsreihenfolge
|
||||
## 12. Implementation Order
|
||||
|
||||
| # | Schritt | Abhängig von |
|
||||
| # | Step | Depends on |
|
||||
|---|---|---|
|
||||
| 1 | Strato-Formulare einreichen, Domainumzug anstoßen | ✅ beauftragt (2026-09-20) |
|
||||
| 2 | Inhaltslage klären (Abschnitt 9) | — |
|
||||
| 3 | Blazor-Projekt in Rider anlegen ✅ (2026-09-20) → nach Gitea pushen ✅ | — |
|
||||
| 6 | Dockerfile, compose.yaml → als Container auf Unraid ✅ (2026-09-20) | 3 |
|
||||
| 4 | Content-Pipeline: Markdig, YamlDotNet, Services, ICS-Endpoint | 3 |
|
||||
| 5 | Layout, Navigation, Seiten mit Platzhaltern | 4 |
|
||||
| 7 | Echte Inhalte einpflegen | 2, 5 |
|
||||
| 8 | Impressum und Datenschutzerklärung | 7 |
|
||||
| 9 | DNS umstellen, NPM Proxy Host, Let's Encrypt | 1, 6 |
|
||||
| 10 | Gitea Actions + Registry + Watchtower | 6, 9 |
|
||||
| 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 |
|
||||
|
||||
Schritte 1 und 2 laufen unabhängig vom Code und sollten sofort starten —
|
||||
Schritt 2 ist der einzige, bei dem Warten echten Schaden anrichtet.
|
||||
Steps 1 and 2 run independently of the code and should start immediately —
|
||||
step 2 is the only one where waiting does real damage.
|
||||
|
||||
**Abweichung von der ursprünglichen Reihenfolge:** Schritt 6 (Deployment) wurde
|
||||
bewusst **vor** die Content-Pipeline (4/5) gezogen. Grund: die riskanteste Kette
|
||||
— Image bauen → Gitea-Registry → auf Unraid ziehen → Container läuft — früh
|
||||
beweisen, statt sie erst kurz vor dem Livegang zu entdecken. Details in
|
||||
`docs/deployment.md`.
|
||||
**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`.
|
||||
|
||||
### Stand 2026-09-20 (abends)
|
||||
### As of 2026-09-20 (evening)
|
||||
|
||||
Erreicht: Das rohe „Hello world" der Blazor-App läuft als Container auf Unraid
|
||||
(`Cube`), erreichbar im LAN unter `http://cube:5000`. Bewiesen ist damit die
|
||||
komplette Deploy-Kette inkl. privater Gitea-Registry.
|
||||
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.
|
||||
|
||||
Bewusste **Test-Abweichungen** vom Produktivziel (Abschnitt 8), später
|
||||
zurückzubauen:
|
||||
Deliberate **test deviations** from the production target (section 8), to be
|
||||
rolled back later:
|
||||
|
||||
- `compose.yaml` hat ein Port-Mapping `5000:8080`. Produktiv: kein Mapping, nur
|
||||
über das externe `npm`-Netz (NPM ist noch nicht testbar, Domain zieht erst um).
|
||||
- Image-Tag nur `:latest`, noch kein SHA-Tag (→ Schritt 10, Rollback).
|
||||
- Registry-Token liegt auf Unraid im Klartext (`/root/.docker/config.json`).
|
||||
Credential-Helper ist als späterer Punkt in `docs/deployment.md` notiert.
|
||||
- `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`.
|
||||
|
||||
**Nächster Schritt:** Content-Pipeline (Schritt 4) — Markdig + YamlDotNet,
|
||||
Services für Seiten/Termine, ICS-Endpoint. Parallel offen und unabhängig vom
|
||||
Code: Inhaltslage klären (Schritt 2, zeitkritisch).
|
||||
**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).
|
||||
|
||||
### Stand 2026-09-21 (vormittags)
|
||||
### As of 2026-09-21 (morning)
|
||||
|
||||
Deployment weiter ausgebaut und einmal komplett durchgespielt:
|
||||
Deployment further built out and run through completely once:
|
||||
|
||||
- **Branch/PR-Workflow** etabliert: nie direkt auf `main`; Feature-Branch → Pull
|
||||
Request in Gitea → Merge. Erstmals durchgeführt (PR #1).
|
||||
- **Zwei Build-Skripte** in `scripts/`: `dev-build.sh` (lokales Dev-Image, kein
|
||||
Push — Dev-Stände bleiben aus der Registry raus) und `release.sh` (baut aus
|
||||
`main`, taggt `:latest` **und** Commit-Kurz-SHA, pusht beide; bricht ab, wenn
|
||||
nicht auf `main` oder Arbeitsverzeichnis unsauber).
|
||||
- **SHA-Tagging** ist damit Standard → Rollback möglich. Erster Release-Tag:
|
||||
`:8643f2c`. Redeploy auf Unraid (Compose Down/Up) bewusst geübt, läuft.
|
||||
- `.gitattributes` erzwingt LF für `*.sh` (sonst scheitert der Shebang unter
|
||||
Windows).
|
||||
- **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).
|
||||
|
||||
Offen fürs nächste Mal (unverändert): Content-Pipeline (Schritt 4) und die
|
||||
zeitkritische Inhaltslage (Schritt 2). Deployment-Automatisierung per Gitea
|
||||
Actions (Schritt 10) ist der nächste optionale Deployment-Ausbau, aber nicht
|
||||
dringend — der Handbetrieb über `release.sh` reicht.
|
||||
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.
|
||||
Reference in new issue
Block a user