Skip to content

Release 1.0.0 - #336

Merged
nichtLehdev merged 191 commits into
mainfrom
release/1.0.0
Sep 18, 2026
Merged

nichtLehdev merged 191 commits into
mainfrom
release/1.0.0

Conversation

@nichtLehdev

@nichtLehdev nichtLehdev commented Sep 17, 2026

Copy link
Copy Markdown
Owner

Sammel-PR für den Meilenstein 1.0.0: 145 Commits, 386 Dateien.

Enthält inzwischen auch die Spiele (#346, #348), den Abschlusstest der Anmeldung samt Korrekturen (#337, #341, #342, #343) und die Umstellung auf Nachrücken von Hand (#341).

Inhalt

Neugestaltung (#319) — Die gesamte Plattform, öffentlich wie Dashboard, steht im Programmheft-Design: Papier und Tinte, Haarlinien statt Karten, Druckorange als einziger Akzent, keine Radien und Schatten. Inhalte, Texte und Seitenaufbau bleiben, soweit nicht ausdrücklich anders entschieden.

Anmeldungen (#314, #316, #317 — schließt #305 und #315)

  • Hinweis im Formular, wenn nicht alle Plätze reichen; volle Preiskategorie führt auf die Warteliste statt in einen Fehler; keine Überbuchung mehr beim Bearbeiten.
  • Anmeldung aufteilen: Die Anmeldenden wählen, wer die freien Plätze bekommt; die übrigen stehen als eigene, verknüpfte Anmeldung auf der Warteliste. Geschwisterrabatt über beide Teile, eine gemeinsame Mail.
  • Nachrück-Angebote: Passt eine Wartelisten-Anmeldung nur teilweise, hält ein Angebot die Plätze 7 Tage; Annahme teilt wie bei der Anmeldung.
  • Nachrücken zählt Preiskategorien über die id statt über das Label.

Inhalte und Medien

Kleinere Korrekturen aus dem Abend: Gruppierung der Startseiten-Termine, Filterbereich der Terminseite ohne Kategorie und neu geordnet, Suchfeld ohne Fokuskasten, Trennlinien zwischen Beiträgen, Rabattmarke an stornierten Anmeldungen, ausgewählter Tag im Nachtdruck, Anfragen bis 100 MB.

Abhängigkeiten: next 16.3.4, react-Gruppe, dev-tools-Gruppe, zod 4.6.2, @types/node 26.5.1 (#300#304).

Vor dem Ausrollen zu erledigen

Automatisch beim Start des App-Containers (deploy/stack.yaml), nichts von Hand:

  • Migrationen registration_groups, waitlist_promotion_offers, download_file_type_image (alle additiv) — prisma migrate deploy läuft bei jedem Start.
  • Glätten harter Zeilenumbrüche in alten Beschreibungen (chore(deploy): Umbrüche in Beschreibungen beim Start glätten #349) — läuft nach den anderen Backfills, wiederholbar ohne Wirkung.

Von Hand in mStudio:

  1. Cron für Nachrück-Angebote, stündlich: node /app/scripts/trigger-waitlist-offers.mjs. Seit feat(warteliste): Nachrücken nur noch von Hand, Wartende haben Vorrang #341 schließt er nur noch abgelaufene Angebote und erinnert das Kursteam; nachgerückt wird per Knopf im Dashboard.
  2. Grenze für Anfragegrößen am vorgelagerten Webserver auf mindestens 100 MB, passend zu experimental.proxyClientMaxBodySize (fix(import): Anfragen bis 100 MB statt 10 MB #329).

Inhaltlich offen: Förderverein (#308) — Seite aufgeräumt, finale Inhalte fehlen.

Geprüft

Auf dem Release-Stand jeweils lokal, weil die CI nur für main und develop läuft: eslint, tsc --noEmit, jest (780 Tests) und next build — alles grün. Dazu Messungen und Screenshots in den einzelnen PRs (Chromium und WebKit, hell und dunkel, 1440 und 390).

dependabot Bot and others added 30 commits September 14, 2026 07:07
Bumps the nextjs group with 1 update: [next](https://github.com/vercel/next.js).


Updates `next` from 16.3.3 to 16.3.4
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](vercel/next.js@v16.3.3...v16.3.4)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nextjs
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps the react group with 4 updates: [react](https://github.com/react/react/tree/HEAD/packages/react), [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react), [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom) and [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom).


Updates `react` from 19.2.8 to 19.3.0
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.3.0/packages/react)

Updates `@types/react` from 19.2.18 to 19.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `react-dom` from 19.2.8 to 19.3.0
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.3.0/packages/react-dom)

Updates `@types/react-dom` from 19.2.7 to 19.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `@types/react` from 19.2.18 to 19.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@types/react-dom` from 19.2.7 to 19.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: react
- dependency-name: "@types/react"
  dependency-version: 19.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: react
- dependency-name: react-dom
  dependency-version: 19.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: react
- dependency-name: "@types/react-dom"
  dependency-version: 19.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: react
- dependency-name: "@types/react"
  dependency-version: 19.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: react
- dependency-name: "@types/react-dom"
  dependency-version: 19.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: react
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps the dev-tools group with 3 updates: [@eslint/eslintrc](https://github.com/eslint/eslintrc), [eslint](https://github.com/eslint/eslint) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `@eslint/eslintrc` from 3.3.6 to 3.3.7
- [Release notes](https://github.com/eslint/eslintrc/releases)
- [Changelog](https://github.com/eslint/eslintrc/blob/main/CHANGELOG.md)
- [Commits](eslint/eslintrc@eslintrc-v3.3.6...eslintrc-v3.3.7)

Updates `eslint` from 10.8.1 to 10.10.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](eslint/eslint@v10.8.1...v10.10.0)

Updates `typescript-eslint` from 8.67.0 to 8.70.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@eslint/eslintrc"
  dependency-version: 3.3.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-tools
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-tools
- dependency-name: typescript-eslint
  dependency-version: 8.70.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-tools
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [zod](https://github.com/colinhacks/zod) from 4.4.3 to 4.6.2.
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](colinhacks/zod@v4.4.3...v4.6.2)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 4.6.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 26.2.0 to 26.5.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
…buchung beim Bearbeiten

Eine ausgebuchte Preiskategorie ließ die Anmeldung mit "Die Preisoption
… ist ausgebucht." scheitern — auch bei Kursen mit Warteliste. Sie gilt
jetzt wie ein voller Kurs: mit Warteliste kommt die ganze Anmeldung
darauf, ohne wird sie weiterhin abgelehnt. "Automatisch" bei der
Erfassung durch das Team entscheidet genauso. findFullPriceTier liefert
dafür die volle Kategorie, statt zu werfen; assertPriceTierCapacity baut
darauf auf.

updateMyRegistration prüfte die Kurskapazität nur bei Kursen ohne
Warteliste. Mit Warteliste konnte eine bestätigte Anmeldung Teilnehmer
hinzufügen, blieb bestätigt und überbuchte den Kurs. Die Prüfung gilt
jetzt unabhängig von der Warteliste, allerdings nur für bestätigte
Anmeldungen und nur für das, was die Änderung dazu belegt: in einem
bewusst überbuchten Kurs lässt sich weiter die Telefonnummer ändern oder
ein Teilnehmer abmelden. Anmeldungen auf der Warteliste belegen keine
Plätze und werden nicht mehr geprüft — wer wegen einer vollen Kategorie
dort steht, konnte sonst gar nichts mehr ändern. Die Bearbeitungsseite
hält es genauso.
Der Wartelisten-Hinweis erschien nur bei einem komplett vollen Kurs. Bei
2 freien Plätzen und 4 Teilnehmern gab es keinen Hinweis, und der Toast
meldete "Ihre Anmeldung war erfolgreich.", obwohl der Server die ganze
Anmeldung auf die Warteliste gesetzt hatte — nur die Mail stimmte.

Schritt 3 warnt jetzt, sobald die Teilnehmer nicht in die freien Plätze
des Kurses oder einer Preiskategorie passen: mit Warteliste, dass die
Anmeldung nicht aufgeteilt wird und alle gemeinsam nachrücken, ohne
Warteliste mit der Bitte, die Anzahl zu reduzieren. Die Anzahlung ist
dann als "nach Platzbestätigung" ausgewiesen. Der Toast richtet sich
nach dem Status, den der Server vergeben hat.

Die Team-Erfassung zählt eine volle Kategorie ebenfalls als fehlende
Plätze und fragt dann nach der Überbuchung.
Reichten die freien Plätze nicht für alle Teilnehmer, kam die ganze
Anmeldung auf die Warteliste – auch wenn ein Teil der Gruppe noch Platz
gehabt hätte. create und createByStaff nehmen jetzt
confirmedParticipantIndexes entgegen: die Gewählten werden eine
bestätigte Anmeldung, die übrigen eine eigene auf der Warteliste. Beide
tragen dieselbe registrationGroupId, weil Kapazität, Rechnungen und
Anzahlung an der Anmeldung hängen, nicht am Teilnehmer.

Aufgeteilt wird nur, wenn der Kurs eine Warteliste führt und nicht
ohnehin alle Platz haben; beim Team nur ohne ausdrücklichen Status.
Die Auswahl wird in der Transaktion noch einmal gegen Kurs und
Preiskategorien geprüft. Passt sie inzwischen nicht mehr, schlägt das
Absenden fehl, statt still anders aufzuteilen.

Der Geschwisterkindrabatt wird über alle nicht stornierten Teile
berechnet und jedem Teil der Anteil seiner Teilnehmer zugerechnet –
getrennte Geschwister verlieren ihn nicht, auch nicht beim Bearbeiten
oder nachträglichen Gewähren. Genehmigen, Ablehnen und "zum vollen
Preis bestätigen" gelten für alle Teile, mit einer Mail über die Summe.
confirmAtFullPrice setzt den Status nicht mehr auf bestätigt; eine
wartende Anmeldung hätte den Kurs damit überbucht.

Die Anzahlung gilt je Teil und ist erst mit der Platzbestätigung
fällig. Eine "bereits eingegangene" Anzahlung verbucht das Team beim
bestätigten Teil. Statt zweier widersprüchlicher Mails geht eine
gemeinsame hinaus, mit Preis, Anzahlung und Link je Teil; das Kursteam
bekommt eine Benachrichtigung "teilweise Warteliste".

getById liefert die übrigen Teile als groupParts, bei Zugriff über
einen Zugangslink mit eigenem Token.

Migration 20260915120000_registration_groups: nur eine neue Spalte
samt Index.
Reichen die Plätze nicht für alle, bietet Schritt 3 die Wahl: ganze
Anmeldung auf die Warteliste (wie bisher) oder die freien Plätze jetzt
nutzen. Dann wählen die Anmeldenden, wer sie bekommt – vorbelegt in
Listenreihenfolge, ohne Teilnehmer, deren Kategorie voll ist.
Teilnehmer, die nicht mehr passen würden, sind nicht wählbar; eine
leere Auswahl sperrt das Absenden. Werden Geschwister getrennt, sagt
der Hinweis, dass der Rabatt erhalten bleibt.

Die Wahl steht vor Preis und Anzahlung, weil beide ihr folgen: der
Anzahlungsblock gilt aufgeteilt den bestätigten Teilnehmern und nennt
die spätere Anzahlung der wartenden. Die Team-Erfassung bekommt dafür
den Status "Aufteilen".

Nach Änderungen an der Teilnehmerliste beginnt die Auswahl neu. Waren
die Plätze beim Absenden schon vergeben, lädt das Formular sie neu.
Der Toast nennt, wie viele bestätigt sind und wie viele warten.
Die Detailseite einer Anmeldung nennt die übrigen Teile mit Status,
Teilnehmern und Link – über einen Zugangslink mit dessen Token, damit
auch Anmeldende ohne Konto hinüberkommen. "Meine Anmeldungen" und die
Teilnehmerliste im Dashboard kennzeichnen geteilte Anmeldungen, die
Detailseite im Dashboard verlinkt die weiteren Teile.
… reichen

Das Nachrücken bestätigte nur ganze Anmeldungen und blieb an der ersten
stehen, die nicht passte – auch wenn ein Platz frei war und die Familie
mit drei Kindern zwei davon gern genommen hätte.

Werden Plätze frei, geht die Warteliste weiter der Reihe nach durch:
- Passt eine Anmeldung ganz, wird sie bestätigt, wie bisher.
- Passt nur ein Teil, bekommt sie ein Nachrück-Angebot: die Anmeldenden
  wählen, wer nachrückt, oder lehnen ab. Bis dahin hält die Warteliste
  an, höchstens sieben Tage und nie über den Kursbeginn hinaus.
- Abgelehnt oder verfallen: die Plätze gehen an die Nächsten, die
  Anmeldung behält ihren Platz. promotionOfferPassedSeats merkt sich,
  bei wie vielen nutzbaren Plätzen – erst wenn mehr frei werden, kommt
  ein neues Angebot.
- Passt niemand (etwa volle Kategorie), bleibt die Warteliste stehen,
  wie bisher.

Annehmen (acceptPromotionOffer) teilt die Anmeldung wie beim Aufteilen
während der Anmeldung: die Gewählten werden eine bestätigte Anmeldung
derselben Gruppe, die übrigen warten auf ihrem Platz weiter. Preise und
Geschwisterkindrabatt werden über alle Teile der Gruppe neu berechnet,
die Anzahlung je Teil; eine schon eingegangene Anzahlung geht mit zum
bestätigten Teil. Wer weniger wählt, als Plätze frei sind, gibt den
Rest an die Nächsten weiter. Antworten dürfen die Anmeldenden (auch
über den Zugangslink) und das Kursteam; das Team erfährt die Antwort
in der App.

Plätze zählt das Nachrücken jetzt nach Preiskategorie-id statt nach
Label (loadSeatAvailability), wie die übrigen Prüfungen – Teil von #315.
Abgelaufene Angebote schließt jeder Lauf selbst, bevor er entscheidet.
updateStatus räumt ein Angebot, sobald eine Anmeldung nicht mehr wartet.
getById liefert ein laufendes Angebot mit den nutzbaren Plätzen.

Mails: Angebot (mit Frist und Link), Angebot abgelaufen, Erinnerung
ans Kursteam.

Migration 20260915140000_waitlist_promotion_offers: drei neue Spalten
samt Index.
POST /api/cron/waitlist-offers stößt für jeden Kurs mit abgelaufenem
Angebot das Nachrücken an – das schließt das Angebot, gibt die Plätze
an die Nächsten und verschickt die Mails – und erinnert das Kursteam
einmal, sobald ein Angebot in höchstens zwei Tagen abläuft. Die Route
läuft auch ohne SMTP: das Weitergeben der Plätze darf nicht am
Mailversand hängen; offene Erinnerungen gehen dann beim nächsten Lauf.

Auf mittwald braucht es dafür einen mStudio-Cronjob mit
scripts/trigger-waitlist-offers.mjs (Vorschlag: stündlich), eingetragen
in der Deployment-Checkliste. Das Dockerfile kopiert das Skript ins
Image – und jetzt auch trigger-newsletter-cleanup.mjs, das laut
Checkliste dort liegen sollte, aber fehlte.
Läuft ein Angebot, zeigt die Detailseite der Anmeldung die Frist und
dieselbe Auswahl wie beim Aufteilen während der Anmeldung: vorbelegt,
wer in die freien Plätze passt, alle übrigen nicht wählbar. Dazu
"Nachrücken lassen" und "Angebot ablehnen" mit Rückfrage. Sind
inzwischen genug Plätze für alle frei, rücken alle nach. Waren die
Plätze beim Absenden schon vergeben, lädt die Seite sie neu.

Der Hinweis zu den fehlenden Plätzen spricht dort von der wartenden
Anmeldung ("nicht genug für alle 3 Teilnehmer") statt vom Anmelden.
"Meine Anmeldungen" kennzeichnet ein laufendes Angebot mit
"Nachrücken möglich", die Detailseite im Dashboard mit der Frist.
…rien prüfen

updateStatus prüfte beim Bestätigen einer wartenden Anmeldung nur die
Kurskapazität. Das Kursteam konnte so eine Anmeldung in eine volle
Preiskategorie bestätigen – etwa die Wasserburg-Einzelzimmer, während
der Kurs insgesamt noch Plätze hatte.

Die Kategorien werden jetzt mit findFullPriceTier geprüft, nach id wie
in allen übrigen Prüfungen; Teilnehmer aus der Zeit vor der id-Migration
über ein eindeutiges Label. Das Nachrücken zählt seit den vorigen
Commits ebenso.

Closes #315
Archivo mit Breitenachse als Variable für die öffentliche Website (das
Dashboard bleibt bei Inter). Neue Farb-Tokens (Tinte, Papier,
Messing-Tinte, Haarlinie, Nachtdruck), schmale Schnitte, Satzbreite,
Füllzeile, Fokus- und Markierungsfarben sowie reduzierte Bewegung.
Gleiche Links, Texte und Logik; neue Darstellung ohne Rundungen und
Schatten, 44px-Bedienflächen, Menüs mit Tintenrahmen, Tastaturfokus für
die Aufklappmenüs. Banner mit lesbarem Kontrast (Tinte auf Orange).
Titelblatt mit Bildstrecke (anhaltbar, ohne Autoplay bei reduzierter
Bewegung), „Kommende Termine“ als Programmliste mit offenen Anmeldungen
vorn (oranges Datumsfeld, Frist, Anmelden), Aktuelles als Druckspalten
mit Logo ohne Titelbild, Register „Beliebte Seiten & Partner“ und
Schlussseite auf Papier. Inhalte und Texte bleiben unverändert.
PRODUCT.md hält Zielgruppen, Ziele, Markenvorgaben und belegte Inhalte
fest; DESIGN.md beschreibt das Programmheft-Designsystem der
öffentlichen Website (Tokens, Typografie, Regeln, Komponenten) als
Grundlage für den Rollout auf die übrigen Seiten.
„Lust auf Posaunenchor?“ steht als bewusster Blickfang auf voller oranger
Fläche mit der Schaltfläche „Mehr erfahren“. DESIGN.md beschreibt den
Schlussaufruf als eigene Komponente und passt Do/Don't daran an.
…itive

Die Startseite setzt sich jetzt aus wiederverwendbaren Programmheft-
Bausteinen zusammen: Köpfe (Heading, SectionHead, ArrowLink), Abschnitt
(PageSection), Wegzeilen und Registerzeilen (WayList, WayRow), Programm-
zeilen mit Datumsslot und offener Anmeldung (ProgrammeList, ProgrammeRow,
DateSlot), Bezirksmarke, Beitragsspalten (NewsColumns), Schaltflächen
(ButtonLink) und Schlussaufruf (ClosingCall).

Die Startseite rendert unverändert: alle Blöcke stehen pixelgenau an
derselben Position. Neu im CSS sind die blaue Zeilenfüllung für
Förderverein-Zeilen und der Fließtext-Link `.link-ink`.
PublicPage setzt Innenseiten jetzt im Programmheft: ein Seitenkopf auf
Papier (PageHead) mit Brotkrumen, genau einem <h1>, Satzstrich und
Leitsatz; die klebende zweite Titelleiste (PageHeader) entfällt. Die
Seite steht vollständig in Archivo, auch Abschnitte, die noch nicht
übertragen sind.

- `color` entfällt: Bezirksfarben färben keine Köpfe mehr, der
  Förderverein setzt `tone="foerderverein"` (blauer Satzstrich).
- Öffentliche Varianten statt ui/card, ui/badge, ui/alert, die dem
  Dashboard bleiben: Panel, Tag, Note.
- PersonRow/PersonList ersetzen PeopleCard (Förderverein, Posaunenrat).
- Wegzeilen ersetzen ParticipationCard auf Über uns, Spiele, Materialien.
- Kopfzeilen der Kurs-, Termin-, Anmelde- und Ensembleseiten mit
  Etiketten und Bezirksmarke statt weißer Pillen; der negative Abstand
  unter dem alten Kopf entfällt.
- Der Ladezustand von /termine nutzt denselben Seitenkopf.
Alle Texte bleiben, nur die Darstellung wechselt: Einstiegsmöglichkeiten
als Registerzeilen, Förderverein mit blauem Satzstrich, Fakten in
Haarlinienspalten und blau füllenden Wegzeilen, „Warum Posaunenchor?“
als Merkpunkte, Mitgliedsbeiträge im Tabellensatz, Downloads als
Wegzeilen, Ehrungen und Versicherungsfrist als Hinweise, der
Schadensfall als Kasten mit Unterlagenliste. „Noch Fragen?“ schließt die
Seite als oranger Schlussaufruf; der Newsletter steht deshalb davor.

Neue Bausteine: PointList (Merkpunkte) und ValueTable (Beträge).
ParticipationCard und SectionHeader werden nicht mehr gebraucht.
- Beta-Etikett ohne Sperrung und Versalien (Tag); Banner-Varianten
  Warnung und Wartung als Tintenfläche, das ungenutzte „success“ entfällt.
- themeColor im Dunkelmodus auf den Nachtgrund #141517.
- Ungenutzte animate-homepage-*-Animationen entfernt.
- hero-carousel.tsx und upcoming-courses-carousel.tsx gelöscht.
Kurs-, Termin- und Anmeldeseite zeigten die Datenbankwerte (LEHRGANG, KONZERT) statt der Bezeichnungen, die auch das Programm der Startseite nutzt.
…tmachen

- Förderverein-Abschnitt als volle Druckfläche in Fördervereinsblau
  (PageSection surface="foerderverein", `.print-field`: Tinte auch im
  Nachtdruck, Zeilen füllen sich papierweiß).
- Fotofeld neben „Warum Posaunenchor?“ mit Bildnachweis; bis eigene
  Fotos gepflegt sind, aus der Bildstrecke der Startseite.
- Ein Strichsystem: der Seitenkopf schließt vollbreit wie die Abschnitte
  (Nacht: Nachtlinie).
- Längere Vorspanne stehen unter dem Titel statt in der schmalen Spalte.
- Kasten im Nachtdruck gleich gebaut; Ehrungen ohne zweiten Kasten.
- Etiketten sind nie umrandet: Art, Dauer, Ort und Bezirk im kompakten
  Kopf als Meta-Text, der Outline-Ton von Tag entfällt.
- Mitmachen: 5/7-Raster, Tabelle und Hinweis über die volle Spalte,
  bewusster Umbruch „Einstiegs-“.
Auf Wunsch des Eigentümers alternieren die Köpfe ab 64rem wie linke und
rechte Heftseiten (neuer Baustein `Split`, 4/12 Kopf neben 8/12 Inhalt,
Kopf im DOM immer zuerst). Das Raster kehrt damit zu 4/8 zurück; das
Fotofeld neben „Warum Posaunenchor?“ entfällt.
DESIGN.md beschreibt jetzt, was diese Sitzung gebaut hat: Seitenkopf der
Innenseiten (normal und kompakt), Abschnitt mit Split und blauer
Druckfläche, Merkpunkte, Beträge, Personenzeile, Etikett, Hinweis, Kasten,
wiederverwendbarer Schlussaufruf, Banner-Varianten, Schaltflächen auf
Papier und Textlinks im Fließtext. Neu sind die One Field Rule (höchstens
eine blaue Förderverein-Fläche neben Titelblatt und Schlussaufruf) sowie
Do's und Don'ts zu Köpfen, Etiketten und Kästen. Der veraltete Hinweis auf
die alten Bausteine ist ersetzt.
Die Einstiegsseite richtet sich an Neue: Jungbläserarbeit rückt nach
vorn, „Warum Posaunenchor?“ steht vor dem Förderverein, Anrede
durchgehend „du“, aktuelle Förderverein-Angaben. Mitgliedschaft,
Beiträge, Ehrungen und Instrumentenversicherung stehen wörtlich
unverändert auf /mitmachen/mitgliedschaft (Anrede „Sie“), verlinkt aus
Abschnitt, Menü und Fußzeile und aufgenommen in Sitemap und Suche.
Geschwister-Ermäßigung sind 20 % der Lehrgangskosten ab dem zweiten
Kind, die Geschenk-CD gibt es für alle Neumitglieder. Die abgelaufenen
Sonderaktionen 2025 samt Verlosung entfallen.
Beitragsliste und ähnliche Beiträge als Druckspalten mit Haarlinien
statt Karten, Filterleiste flach, Beiträge ohne Titelbild zeigen das
Logo auf Tinte. Beitragsseite mit ruhigem Kopf, Meta-Zeile und
Bildfeld im Text; Lightbox ohne Rundungen und Weichzeichner. Suche
erstmals mit Seitenrahmen und nach Art gruppierten Trefferzeilen.
Newsletter-Seiten als Innenseiten mit flachen Eingabefeldern.
Texte, Filter, RSS und Double-Opt-in unverändert.
…gung

fix(anmeldung): Zahlungs-Knöpfe in Safari, Vorbelegung freier Kategorie
…ingaben

'Mitglied seit' wird nicht gebraucht: Das Feld ist aus Anlegen,
Bearbeiten, Detailseite und Liste entfernt. Die Spalte in der Datenbank
bleibt, Bearbeiten schickt den Wert nicht mehr mit und laesst einen
gespeicherten also unangetastet.

Das Rollen-Auswahlfeld stand niedriger als die Eingabefelder daneben: Der
Auswahl-Baustein setzte standardmaessig text-sm (38px), Input erbt die
normale Groesse (42px). Jetzt erbt auch die Auswahl -- das betrifft jedes
Formular, in dem beide nebeneinander stehen. Kompakte Auswahlfelder in
Filterleisten geben text-sm weiterhin selbst mit.

Gemessen in Chromium und WebKit: Foerderverein und Termine 42/42 px,
Kurse 46/46 px (dort eigens groessere Felder).
fix(förderverein): Ohne „Mitglied seit“, Auswahlfelder so hoch wie Eingaben
Der Erfahrungsbericht von "Michael K., Chorleiter seit 2018" hat nie
jemand gesagt -- ein Platzhalter, der schon vor der Neugestaltung auf der
Seite stand. Der Abschnitt entfaellt; auf die Unterstuetzungsliste folgt
direkt der Schlussaufruf. Es war das einzige Zitat dieser Art auf der
Website.
fix(ehrenamt): Erfundenes Zitat entfernt
Die letzten beiden Bausteine im alten Design. Sie oeffnen sich mitten aus dem
fertigen Setup heraus ("Eigenes Set ...", aus Noten-Lesen und aus Griffe) —
dort traf neues auf altes Aussehen. Gemessen steht der Spielebereich damit
auf 0 Alttreffern, vorher 50 in der Bibliothek und 54 im Editor.

- Huelle: Papier/Nacht mit 2px-Tintenrahmen ab `sm`, Kopf und Fuss auf
  Haarlinien. Schatten und Rundungen ersatzlos weg.
- Auswahl ist ein Druckfeld (`on-orange bg-primary text-ink`): Filter-Chips,
  Schluessel-Chips, Notenchips, Schluessel-Abzeichen.
- Orange-als-Text nur ueber `text-primary-ink` / nachts `text-primary`; die
  Hauptaktionen laufen ueber den gemeinsamen Button in Tinte statt weiss auf
  Orange, was 1,99:1 ergeben haette.
- Rot bleibt Rot, aber gestuft: Loeschen oeffnet als Umriss, die Bestaetigung
  ist gefuellt.
- Klickziele von rund 26-30px auf 44px; Schliessen-Knopf 44x44.

Vorlese-Namen: Das Verschmelzungsproblem der Auswahlkarten gibt es hier
nicht. Das tatsaechliche Problem war Mehrdeutigkeit — in einer Liste mit
mehreren Saetzen hiessen die Knoepfe viermal gleich ("Verwenden", "Link
kopieren", "Bearbeiten", "Loeschen"), je Oktave nochmal ("Stammtoene",
"Leeren"). Jetzt satzbezogen: "<Name> verwenden", "Stammtoene der Oktave 4
hinzufuegen". Sichtbarer Text unveraendert.

Nachgeprueft statt geglaubt: nur diese zwei Dateien geaendert, 0 Alttreffer
je Datei, tsc 0, jest 57 Suiten / 681 Tests gruen.

Drei Entscheidungen, die zur Gegenlesung offenstehen:

- Der Huellenvertrag (GameBarSlot/GameDock) ist hier bewusst NICHT angewandt:
  Die Bibliothek ist ein modaler Dialog ueber dem Spiel, kein Spielschirm;
  eine Fusszeile im Dock laege hinter dem Overlay.
- Der Inline-Link "anmelden" bleibt 14px hoch. 44px wuerden den Satz
  zerreissen, und die Programmheft-Seiten fuehren Textlinks ueberall inline.
- Neu importiert werden `Button` und `fieldControlClasses` aus
  `_components/programmheft`. Das koppelt den Spielebereich an die
  Heft-Bausteine; alternativ waeren es zwei Klassenkonstanten.

Luecke, die offen bleibt: Anlegen, Bearbeiten und Loeschen sind
`protectedProcedure` und wurden nicht gegen den Server durchgespielt — dafuer
braeuchte es eine Anmeldung. Der gesamte Client-Pfad des Editors ist bedient
(Zeichenzaehler, Oktavauswahl, Schluesselwechsel mit Bereichswarnung,
Abbrechen, Loeschruecknahme), nur die Serverantwort auf "Speichern" fehlt.
CodeQL meldete am Release-PR vier Warnungen hoher Stufe im Klartext-
Helfer aus #311:

- Tags wurden in einem Durchgang entfernt. Aus <<b>script> macht das
  <script> -- das Entfernen selbst setzt ein neues Tag zusammen. Jetzt
  wird wiederholt, bis sich nichts mehr aendert.
- Entitaeten wurden nach dem Abraeumen und nacheinander aufgeloest: Aus
  &lt;script&gt; wurde hinterher wieder <script>, und &amp;lt; wurde
  doppelt entschluesselt. Jetzt in einem Durchgang und danach noch einmal
  abgeraeumt.

Der Klartext wird heute nirgends als HTML ausgefuehrt (iCal, Metadaten,
Suche), soll aber von sich aus frei von Tags sein. Spitze Klammern im
Text ("Kinder <10 Jahre", "a < b") bleiben stehen. Die Test-Hilfsfunktion
folgt denselben Regeln; neue Tests fuer die Angriffsmuster.
fix(sicherheit): Klartext-Helfer lässt garantiert keine Tags stehen
Ein `<button>` nimmt nur Phrasing-Inhalt; `<p>` gehoert nicht dazu. Die
Setup-Karten der Griffe rendern den Hinweistext an fuenf Stellen als `<p>`
im Knopf — ungueltiges Markup, das Browser zwar aufraeumen, das aber beim
Hydrieren warnt und die Baumstruktur unnoetig unklar laesst. In Noten-Lesen
war dasselbe Muster bereits korrigiert, hier blieb es stehen.

Fuenfmal `<span>` mit `block`, damit der Hinweis weiter eine eigene Zeile
bekommt. Die drei `<p>` der Gruppenbeschriftungen stehen ausserhalb der
Knoepfe und bleiben.

Nachgemessen: `<p>` in `<button>` von 5 auf 0; alle zehn Hinweise in hell und
nacht `display: block`, eigene Zeile, sichtbar (17px einzeilig, 33px
zweizeilig). Also rein strukturell, optisch folgenlos.
Spiele: Notensatz-Bibliothek im Programmheft und ungueltiges Markup
Auf mittwald lassen sich im laufenden Container keine Skripte ausfuehren.
Das Skript gegen harte Zeilenumbrueche laeuft deshalb wie die anderen
Backfills im Startbefehl (deploy/stack.yaml), direkt nach den Migrationen
-- die ohnehin schon bei jedem Start automatisch eingespielt werden.

Damit das bei jedem Neustart gefahrlos ist, liegt die Regel jetzt getestet
in src/lib/description-linebreaks.ts: Der Editor speichert Zeilenumbrueche
als Markdown-Umbruch mit zwei Leerzeichen am Zeilenende, solche Zeilen
fasst sie nie an, Listen und Absaetze auch nicht, und ein zweiter Lauf
aendert nichts mehr. Das Skript schreibt jetzt standardmaessig (wie die
anderen Backfills), --dry-run zeigt nur.

Die neue Datei steht in der Kopierliste des Dockerfiles; ohne sie waere
das Skript beim Start still gescheitert. Nachgewiesen im lokal gebauten
Laufzeit-Image: Skript laeuft, erreicht die Datenbank, meldet auf dem schon
geglaetteten Bestand 0 Aenderungen.
chore(deploy): Umbrüche in Beschreibungen beim Start glätten
Die Wartungsseite rendert auf dem Server, und der laeuft in UTC. Ohne
feste Zeitzone stand dort 21:59, wo im Dashboard 23:59 eingetragen war
(im Winter 22:59). Jetzt Europe/Berlin, und mit Wochentag:
"Donnerstag, 17. September 2026 um 23:59".
fix(wartung): Voraussichtliches Ende in deutscher Ortszeit
Die App laeuft in Produktion in einem Container mit Zeitzone UTC. Ueberall,
wo ohne feste Zeitzone formatiert wurde, standen Datum und Uhrzeit in UTC:
zwei Stunden zu frueh (im Winter eine), und ein Termin kurz nach
Mitternacht am Vortag. Auch Client-Komponenten waren betroffen, weil sie
zuerst auf dem Server gerendert werden (danach Hydration-Abweichung), und
E-Mails, die immer auf dem Server entstehen.

Neuer Helfer src/lib/berlin-time.ts: feste Formate in Europe/Berlin
(Ausgabe Zeichen fuer Zeichen wie vorher), Kalenderbestandteile in Berliner
Zeit als Ersatz fuer getDate() und Co., berlinDate fuer Berliner Uhrzeiten
auch an den Umstellungstagen. Getestet unter Sommer- und Winterzeit, an
der Mitternachtsgrenze und in Prozessen unter UTC, Los Angeles und
Kiritimati.

Umgestellt: oeffentliche Seiten, Kalender, Anmeldeformular, Dashboard,
zehn Mailvorlagen samt Kursmail-Platzhaltern und Newsletter, Exporte und
Dateinamen. Wo es um einen deutschen Kalendertag geht, rechnet auch die
Logik in Berlin: Kalenderzellen, heute, Monatsgruppen, vergangen/kommend,
Altersgrenze der Preiskategorie (Server und Formular waren sich bei einem
Kurs ab 00:00 uneinig), Monats- und Jahresgrenzen in Abfragen, Statistik.
Dabei behoben: Termin bearbeiten belegte das Datum aus dem UTC-Tag vor,
ein Termin zwischen 00:00 und 02:00 waere auf den Vortag gerutscht.

Eingabefelder rechnen weiter in der Browserzeit; das ist dort richtig.

Gemessen, UTC-Server gegen Berlin-Server: im Server-HTML vorher 29 von
108 Zeitangaben abweichend, nachher 0; im Dashboard vorher 83 von 245,
nachher 0; Mailvorlagen vorher 10 von 12 falsch, nachher 0; keine
Hydration-Warnungen mehr.
fix(zeit): Alle Datums- und Zeitangaben in deutscher Ortszeit
Die Frist endete mit setHours(23, 59, 59, 999) in der Zeit des Rechners.
Der Server läuft in UTC; dort war das Ende 01:59 bzw. 00:59 deutscher
Zeit am Folgetag, Anmeldungen gingen also bis zu zwei Stunden zu spät
durch. Um Mitternacht gespeicherte Fristen (22:00 UTC am Vortag) liefen
sogar schon am Vorabend ab, fast einen Tag zu früh. Jetzt endet die Frist
mit dem deutschen Kalendertag.

Die fortlaufende Rechnungsnummer nahm das Jahr aus getFullYear() in UTC:
Rechnungen vom 1. Januar zwischen 00:00 und 01:00 Uhr bekamen noch das
alte Jahr. invoiceYear() nimmt das Berliner Jahr.

Die Tests bauen ihre Zeitpunkte über berlinDate und laufen damit in jeder
Zeitzone gleich (geprüft von UTC−11 bis UTC+14).
fix: Anmeldefrist und Rechnungsjahr in deutscher Zeit
Viele Kommentare erzählten Verlauf, Messwerte und Abwägungen nach, statt
den einen nicht offensichtlichen Punkt zu nennen. Entfernt sind
Wiederholungen des Codes, Abschnittsmarken, Banner, auskommentierter Code,
leere JSDoc und Verweise auf frühere Stände, PRs und Personen. Lange
Erklärungen sind auf ihren Grund gekürzt.

Erhalten bleiben Browser-Eigenheiten, die Regel zu UTC und deutscher Zeit,
Sicherheits-, Datenschutz- und Geschäftsregeln sowie alle Direktiven
(eslint-disable, @ts-expect-error, prettier-ignore usw.).

Nur Kommentare geändert: Für jede Datei ist der Syntaxbaum gleich (TS/JS),
bzw. der Inhalt ohne Kommentarzeilen (CSS, Prisma, YAML, Shell,
Dockerfile). Kommentarzeilen 12.123 -> 6.077, Blöcke ab 6 Zeilen 495 -> 15.
Zwei falsche Kommentare sind weg: users.delete ist kein Soft-Delete, und
die JSDoc über sendConfirmationEmail gehörte zu einer anderen Funktion.
chore: Kommentare kürzen und überflüssige entfernen
Die Mails zur aufgeteilten Anmeldung und zum Nachrück-Angebot (samt
„Angebot abgelaufen“ und Erinnerung ans Kursteam) bauten noch ihr eigenes
Gerüst im alten Design: weiße Schrift auf Orange, runde Kästen, orange
Knöpfe. Sie nutzen jetzt EmailLayout mit Regel, Knopf und Ersatzadresse
wie alle anderen Mails und gehen zusätzlich als Nur-Text-Fassung raus.

Der Warteliste-Kasten im Dashboard sprach salopp („Automatisch rückt
niemand nach – das löst du hier aus“). Die Texte im Kasten, in der
Rückfrage, im Ergebnis und in den Haltegründen sind jetzt sachlich und
ohne direkte Anrede. Das Ergebnis nennt „die Anmeldung von …“, damit
klar ist, dass nicht die Anmeldende selbst nachgerückt ist.
fix: Warteliste-Mails im neuen Design, sachliche Texte im Team-Kasten
@nichtLehdev
nichtLehdev merged commit fb87d4b into main Sep 18, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants