diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 2571ebf1..eef734f6 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,6 +1,6 @@
version: 2
updates:
- # Enable version updates for npm (works with pnpm too)
+ # "npm" also covers pnpm.
- package-ecosystem: "npm"
directory: "/"
schedule:
@@ -12,38 +12,30 @@ updates:
labels:
- "dependencies"
- "automated"
- # Group updates to reduce PR noise
groups:
- # Group all Next.js related packages together
nextjs:
patterns:
- "next"
- "eslint-config-next"
- # Group all Prisma packages together
prisma:
patterns:
- "@prisma/*"
- "prisma"
- # Group all tRPC packages together
trpc:
patterns:
- "@trpc/*"
- # Group all React Query packages together
react-query:
patterns:
- "@tanstack/react-query*"
- # Group all TipTap editor packages together
tiptap:
patterns:
- "@tiptap/*"
- # Group React core packages
react:
patterns:
- "react"
- "react-dom"
- "@types/react"
- "@types/react-dom"
- # Group TypeScript and ESLint tooling
dev-tools:
patterns:
- "typescript"
@@ -52,24 +44,18 @@ updates:
- "typescript-eslint"
- "prettier"
- "prettier-plugin-*"
- # Group Tailwind packages
tailwind:
patterns:
- "tailwindcss"
- "@tailwindcss/*"
- # Ignore specific updates if needed
ignore:
- dependency-name: "next"
update-types: ["version-update:semver-major"]
- # typescript-eslint hard-throws on any TS >= 7 (unconditional version guard,
- # no flag or env var to opt out), so a TS 7 bump breaks `pnpm lint` entirely.
- # Upstream is targeting TS >= 7.1 and skipping 7.0:
+ # typescript-eslint throws on TS >= 7 and breaks `pnpm lint`; drop once it supports TS 7.1:
# https://github.com/typescript-eslint/typescript-eslint/issues/10940
- # Drop this entry once typescript-eslint ships TS 7.1 support.
- dependency-name: "typescript"
update-types: ["version-update:semver-major"]
- # Enable version updates for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
@@ -83,7 +69,6 @@ updates:
- "github-actions"
- "automated"
- # Enable version updates for Docker
- package-ecosystem: "docker"
directory: "/"
schedule:
diff --git a/.github/workflows/build-verification.yml b/.github/workflows/build-verification.yml
index 1fddbc87..62bc6b24 100644
--- a/.github/workflows/build-verification.yml
+++ b/.github/workflows/build-verification.yml
@@ -85,9 +85,6 @@ jobs:
SKIP_ENV_VALIDATION: 1
NODE_ENV: production
NEXT_TELEMETRY_DISABLED: 1
- # Add other non-secret env vars needed for build
- # BETTER_AUTH_SECRET: dummy-secret-for-build
- # BETTER_AUTH_URL: http://localhost:3000
- name: Check build output
run: |
diff --git a/.github/workflows/database-validation.yml b/.github/workflows/database-validation.yml
index 0ec05959..0253f112 100644
--- a/.github/workflows/database-validation.yml
+++ b/.github/workflows/database-validation.yml
@@ -28,8 +28,7 @@ jobs:
with:
node-version: "22"
- # Ohne `version` liest die Action das `packageManager`-Feld aus
- # package.json — dieselbe Quelle, die auch pnpm selbst verwendet.
+ # Ohne `version` liest die Action `packageManager` aus package.json.
- name: Setup pnpm
uses: pnpm/action-setup@v6
@@ -61,11 +60,8 @@ jobs:
env:
DATABASE_URL: postgresql://dummy:dummy@localhost:5432/dummy
- # `prisma validate` prüft nur, ob das Schema in sich stimmig ist. Es sagt
- # nichts darüber, ob die Migrationen auf einer leeren Datenbank überhaupt
- # durchlaufen — und genau das passiert beim Deploy auf mittwald. Schlägt es
- # dort fehl, startet die App nicht (30 Versuche, dann Abbruch). Dieser Job
- # zieht den Fehlschlag in die PR vor.
+ # `prisma validate` prüft nicht, ob die Migrationen auf leerer Datenbank laufen;
+ # scheitern sie beim Deploy auf mittwald, startet die App nicht.
migrate-deploy:
name: Migrations & Drift
runs-on: ubuntu-latest
@@ -118,15 +114,12 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- # Der eigentliche Test: alle Migrationen auf einer jungfräulichen
- # Datenbank, so wie beim ersten Start auf einem neuen Server.
+ # Alle Migrationen auf leerer Datenbank, wie beim ersten Start auf neuem Server.
- name: Apply all migrations from scratch
run: pnpm prisma migrate deploy
- # Fängt den Fall ab, dass jemand schema.prisma ändert und die passende
- # Migration vergisst: die Datenbank läuft dann auseinander mit dem
- # Client, den `prisma generate` erzeugt. --exit-code liefert 2, wenn ein
- # Unterschied bleibt.
+ # Fängt eine vergessene Migration zu einer schema.prisma-Änderung ab;
+ # --exit-code liefert 2 bei Unterschied.
- name: Check for schema drift
run: >-
pnpm prisma migrate diff
@@ -134,17 +127,14 @@ jobs:
--to-schema prisma/schema.prisma
--exit-code
- # Beide Skripte laufen bei jedem Produktions-Deploy automatisch und sind
- # dort bewusst "non-fatal" — ein Fehler würde also still durchgehen.
- # Hier dürfen sie scheitern.
+ # Beim Deploy sind beide Skripte "non-fatal"; hier sollen Fehler auffallen.
- name: Run post-migration setup
run: pnpm tsx prisma/post-migration-setup.ts
- name: Run slug backfill
run: pnpm tsx prisma/backfill-slugs.ts
- # Wiederholbarkeit ist die Eigenschaft, auf die sich die Deploy-Skripte
- # verlassen: Sie laufen bei *jedem* Start, nicht nur beim ersten.
+ # Die Deploy-Skripte laufen bei *jedem* Start, müssen also wiederholbar sein.
- name: Re-run setup and backfill (idempotency)
run: |
pnpm tsx prisma/post-migration-setup.ts
diff --git a/.github/workflows/release-deploy.yml b/.github/workflows/release-deploy.yml
index d3e1c936..2a9937ab 100644
--- a/.github/workflows/release-deploy.yml
+++ b/.github/workflows/release-deploy.yml
@@ -1,19 +1,5 @@
-# =============================================================================
-# Release & Deploy Workflow
-# =============================================================================
-# Triggers on every published release, INCLUDING pre-releases.
-#
-# build-and-push: builds Docker images and pushes them to GHCR with
-# semver tags; "latest" is only applied to full releases
-# deploy-next: old server (pwr.lehdev.de) via SSH — receives every
-# release AND every pre-release (bleeding edge)
-# deploy-production: mittwald container stack via deploy-container-action —
-# full releases only
-#
-# GitHub environments used: "next" (old server SSH secrets) and
-# "production" (mittwald API token, stack id, app secrets — see deploy/stack.yaml
-# for the full list of values the stack file expects).
-# =============================================================================
+# Runs on every release incl. pre-releases: the old server (env "next") gets all of them,
+# mittwald (env "production", values see deploy/stack.yaml) only full releases.
name: Release & Deploy
@@ -21,7 +7,6 @@ on:
release:
types: [published]
-# Prevent concurrent deployments
concurrency:
group: release-deploy
cancel-in-progress: false
@@ -34,9 +19,6 @@ env:
CRON_IMAGE_NAME: nichtlehdev/pwr-cron
jobs:
- # ===========================================================================
- # Build and Push Docker Images (all releases, including pre-releases)
- # ===========================================================================
build-and-push:
name: Build & Push Docker Image
runs-on: ubuntu-latest
@@ -46,8 +28,7 @@ jobs:
packages: write
outputs:
- # Clean semver version without leading "v" (e.g. 1.4.2 or 1.5.0-rc.1);
- # used by the deploy jobs to pin the exact image tag.
+ # Semver without leading "v" (e.g. 1.5.0-rc.1); pins the image tag for the deploys.
version: ${{ steps.meta.outputs.version }}
image-tag: ${{ steps.meta.outputs.tags }}
image-digest: ${{ steps.build.outputs.digest }}
@@ -136,9 +117,6 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
- # ===========================================================================
- # Deploy to Old Server (every release and pre-release)
- # ===========================================================================
deploy-next:
name: Deploy to Old Server (next)
runs-on: ubuntu-latest
@@ -173,34 +151,22 @@ jobs:
port: ${{ secrets.SERVER_PORT }}
envs: CR_PAT
script: |
- # Navigate to project directory
cd ${{ secrets.SERVER_PROJECT_PATH }}
- # Pin the exact release version for docker compose interpolation
- # (pre-releases are never tagged "latest")
+ # Pre-releases are never tagged "latest", so pin the exact version.
export APP_IMAGE_TAG=${{ needs.build-and-push.outputs.version }}
- # Login to GitHub Container Registry
echo "${{ secrets.CR_PAT }}" | docker login ghcr.io -u nichtlehdev --password-stdin
- # Pull the release images
docker pull ghcr.io/nichtlehdev/pwr-web:$APP_IMAGE_TAG
docker pull ghcr.io/nichtlehdev/pwr-backup:$APP_IMAGE_TAG
docker pull ghcr.io/nichtlehdev/pwr-cron:$APP_IMAGE_TAG
- # Update the running containers
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
- # Cleanup old images.
- #
- # This server receives every pre-release, so each deploy leaves
- # three more tagged images behind — that, not dangling layers, is
- # what fills the disk. Only our own repositories are touched, and
- # only tags other than the one just deployed (plus "latest", which
- # a manual `docker compose up` without APP_IMAGE_TAG falls back
- # to). Images belonging to anything else on this server are left
- # alone, which `docker image prune -af` would not do.
+ # Every pre-release leaves three tagged images behind. Remove only our own old tags
+ # ("latest" stays as fallback without APP_IMAGE_TAG); `prune -af` would hit other images.
echo "Disk before cleanup:"
docker system df --format 'table {{.Type}}\t{{.Size}}\t{{.Reclaimable}}' || true
@@ -213,8 +179,6 @@ jobs:
| xargs -r docker rmi 2>/dev/null || true
done
- # Dangling layers left over from the above, plus stopped
- # containers' writable layers.
docker image prune -f
echo "Disk after cleanup:"
@@ -233,7 +197,6 @@ jobs:
# Wait for container to be healthy
sleep 10
- # Check if containers are running
if docker ps | grep -q posaunenwerk-app; then
echo "✅ App container is running"
else
@@ -258,9 +221,6 @@ jobs:
exit 1
fi
- # ===========================================================================
- # Deploy to mittwald (full releases only)
- # ===========================================================================
deploy-production:
name: Deploy to mittwald (production)
runs-on: ubuntu-latest
@@ -279,8 +239,7 @@ jobs:
- name: Deploy stack to mittwald
uses: mittwald/deploy-container-action@v1
env:
- # Every {{ .Env.* }} placeholder in deploy/stack.yaml must be
- # defined here (see the stack file header for the full list).
+ # Every {{ .Env.* }} placeholder in deploy/stack.yaml must be defined here.
RELEASE_VERSION: ${{ needs.build-and-push.outputs.version }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
APP_URL: ${{ vars.APP_URL }}
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 00000000..fed18f9b
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,360 @@
+---
+name: Posaunenwerk Rheinland
+description: Das Programmheft – weißes Programmpapier, orange Druckflächen mit tiefschwarzer Schrift, Archivo in zwei Breiten, Tabellensatz mit Haarlinien.
+colors:
+ primary: "#faa619"
+ primary-ink: "#a55800"
+ foerderverein: "#78a8da"
+ ink: "#1c1d1f"
+ paper: "#ffffff"
+ slate: "#58595b"
+ rule: "#d4d4d1"
+ night: "#141517"
+ night-raised: "#1d1f22"
+ night-rule: "#34373c"
+ night-text: "#ecebe8"
+ night-muted: "#a6a8ad"
+ cancelled: "#b91c1c"
+ cancelled-night: "#f87171"
+ district-1: "#3b82f6"
+ district-2: "#10b981"
+ district-3: "#8b5cf6"
+ district-4: "#f59e0b"
+ district-5: "#ef4444"
+ district-6: "#06b6d4"
+ district-7: "#ec4899"
+ district-8: "#14b8a6"
+ district-9: "#6366f1"
+ district-10: "#84cc16"
+ district-11: "#f97316"
+ district-12: "#a855f7"
+ district-13: "#22d3ee"
+typography:
+ display:
+ fontFamily: "Archivo, ui-sans-serif, system-ui, sans-serif"
+ fontSize: "clamp(2.75rem, 6vw, 5.75rem)"
+ fontWeight: 800
+ lineHeight: 0.9
+ letterSpacing: "-0.01em"
+ fontVariation: "'wdth' 68"
+ headline:
+ fontFamily: "Archivo, ui-sans-serif, system-ui, sans-serif"
+ fontSize: "clamp(2.25rem, 4.5vw, 3.5rem)"
+ fontWeight: 800
+ lineHeight: 0.95
+ fontVariation: "'wdth' 68"
+ programm-head:
+ fontFamily: "Archivo, ui-sans-serif, system-ui, sans-serif"
+ fontSize: "2.25rem"
+ fontWeight: 800
+ lineHeight: 1
+ fontVariation: "'wdth' 68"
+ list-head:
+ fontFamily: "Archivo, ui-sans-serif, system-ui, sans-serif"
+ fontSize: "1.75rem"
+ fontWeight: 800
+ lineHeight: 1
+ fontVariation: "'wdth' 68"
+ title:
+ fontFamily: "Archivo, ui-sans-serif, system-ui, sans-serif"
+ fontSize: "1.5rem"
+ fontWeight: 700
+ lineHeight: 1.1
+ fontVariation: "'wdth' 68"
+ date-numeral:
+ fontFamily: "Archivo, ui-sans-serif, system-ui, sans-serif"
+ fontSize: "2.75rem"
+ fontWeight: 800
+ lineHeight: 1
+ fontFeature: "'tnum' 1"
+ fontVariation: "'wdth' 68"
+ lead:
+ fontFamily: "Archivo, ui-sans-serif, system-ui, sans-serif"
+ fontSize: "clamp(1.25rem, 2.1vw, 1.875rem)"
+ fontWeight: 500
+ lineHeight: 1.375
+ fontVariation: "'wdth' 84"
+ body:
+ fontFamily: "Archivo, ui-sans-serif, system-ui, sans-serif"
+ fontSize: "0.9375rem"
+ fontWeight: 400
+ lineHeight: 1.5
+ meta:
+ fontFamily: "Archivo, ui-sans-serif, system-ui, sans-serif"
+ fontSize: "0.875rem"
+ fontWeight: 600
+ lineHeight: 1.25
+ fontVariation: "'wdth' 84"
+ button:
+ fontFamily: "Archivo, ui-sans-serif, system-ui, sans-serif"
+ fontSize: "1rem"
+ fontWeight: 600
+ lineHeight: 1
+ fontVariation: "'wdth' 84"
+rounded:
+ none: "0px"
+ avatar: "9999px"
+spacing:
+ sheet-mobile: "1.25rem"
+ sheet-tablet: "2.5rem"
+ sheet-desktop: "3.5rem"
+ sheet-max: "100rem"
+ row-y: "1rem"
+ section-y: "4rem"
+ section-y-md: "6rem"
+components:
+ button-primary:
+ backgroundColor: "{colors.ink}"
+ textColor: "{colors.paper}"
+ typography: "{typography.button}"
+ rounded: "{rounded.none}"
+ padding: "0 24px"
+ height: "48px"
+ button-primary-hover:
+ backgroundColor: "{colors.paper}"
+ textColor: "{colors.ink}"
+ button-register:
+ backgroundColor: "transparent"
+ textColor: "{colors.ink}"
+ typography: "{typography.button}"
+ rounded: "{rounded.none}"
+ padding: "0 16px"
+ height: "40px"
+ button-register-hover:
+ backgroundColor: "{colors.paper}"
+ button-icon:
+ backgroundColor: "transparent"
+ textColor: "{colors.ink}"
+ rounded: "{rounded.none}"
+ size: "44px"
+ button-icon-hover:
+ backgroundColor: "{colors.ink}"
+ textColor: "{colors.paper}"
+ date-tile-open:
+ backgroundColor: "{colors.primary}"
+ textColor: "{colors.ink}"
+ typography: "{typography.date-numeral}"
+ rounded: "{rounded.none}"
+ padding: "8px 8px 10px"
+ width: "80px"
+ way-row:
+ backgroundColor: "{colors.paper}"
+ textColor: "{colors.ink}"
+ typography: "{typography.title}"
+ rounded: "{rounded.none}"
+ padding: "0 4px"
+ height: "56px"
+ way-row-hover:
+ backgroundColor: "{colors.primary}"
+ textColor: "{colors.ink}"
+ nav-link:
+ textColor: "{colors.slate}"
+ typography: "{typography.button}"
+ height: "44px"
+ nav-link-active:
+ textColor: "{colors.ink}"
+ menu-panel:
+ backgroundColor: "{colors.paper}"
+ textColor: "{colors.ink}"
+ rounded: "{rounded.none}"
+ padding: "4px 0"
+ menu-panel-item-hover:
+ backgroundColor: "{colors.primary}"
+ textColor: "{colors.ink}"
+ banner-info:
+ backgroundColor: "{colors.primary}"
+ textColor: "{colors.ink}"
+ padding: "8px 16px"
+---
+
+# Design System: Posaunenwerk Rheinland
+
+## Overview
+
+**Creative North Star: "Das Programmheft"**
+
+Die öffentliche Website ist gesetzt wie das Programmheft eines Bläsertags: weißes Programmpapier, volle orange Druckflächen mit tiefschwarzer Schrift, Termine im Tabellensatz mit festen Datumsspalten und 1px-Haarlinien. Die Schrift ist eine Familie in zwei Breiten: Archivo schmal und extrafett für Titel, Daten und Köpfe (ein Echo der Logo-Wortmarke), normal breit für Lesetext. Die Wirkung soll lebendig, verlässlich und unverwechselbar „unser Verband“ sein, nicht kirchlich-altbacken, agentur-schick, verspielt-kindlich oder behördlich-nüchtern.
+
+Die Tiefe entsteht aus Druckmitteln, nicht aus Licht: Linien in zwei Stärken (2px Tinte als Kopf- und Abschnittsstrich, 1px Haarlinie zwischen Zeilen), Farbflächen und Anschnitt. Inhalte stehen in Zeilen und Spalten statt in Karten; Wege sind volle Zeilen, die sich beim Zeigen von links orange füllen. Der Dunkelmodus („Nachtdruck“) setzt dasselbe Heft auf einen fast schwarzen Grund; Orange bleibt Druckfarbe.
+
+Das Programmheft gilt für die Startseite, die geteilte Chrome (Navigation, Beta-Banner, Fußzeile) und den Kopf aller Innenseiten (`PublicPage`); /mitmachen ist die Referenz-Innenseite. Die Bausteine liegen in `src/app/_components/programmheft/`. Die übrigen Abschnitte der Innenseiten sowie EventCard, CourseCard und PostCard tragen noch die alte Welt, stehen übergangsweise schon in Archivo und werden seitengruppenweise übertragen; sie sind nicht Teil dieses Systems. `ui/card`, `ui/badge` und `ui/alert` gehören dem Dashboard, die öffentliche Seite nutzt Kasten (Panel), Etikett (Tag) und Hinweis (Note). Das Dashboard bleibt vorerst bei Inter und seinen eigenen Tokens.
+
+**Key Characteristics:**
+- Papier, Tinte und eine Druckfarbe (Orange); Blau nur für den Förderverein.
+- Archivo über die Breitenachse: schmal-fett (`wdth` 68) spricht, halbschmal (`wdth` 84) etikettiert, normal liest.
+- Tabellensatz: feste Datumsslots mit Tabellenziffern, Haarlinien zwischen Zeilen, 2px-Striche unter Köpfen.
+- Keine Karten, Schatten, Rundungen, Icon-Kreise oder Farbstreifen.
+- Wege als volle Zeilen mit Pfeil, die sich bei Hover/Fokus von links orange füllen.
+
+## Colors
+
+Eine Druckfarbe auf Papier und Tinte, in zwei Druckgängen (Tag und Nacht).
+
+### Primary
+- **Druckorange** (#faa619): Die verbindliche Markenfarbe als volle Fläche: Titelblatt, Schlussaufruf („Lust auf Posaunenchor?“, „Noch Fragen?“), Datumsfeld bei offener Anmeldung, Frist-Hervorhebung ≤ 7 Tage, Hinweis „wichtig“ (z. B. Kündigungsfrist), Zeilenfüllung bei Hover/Fokus, Hover in Menüs, Beta-Banner, Aktiv-Unterstrich der Navigation, Textmarkierung. Auf Orange steht immer Tinte. Im Nachtdruck ist Druckorange auch Textfarbe (Links, Status „Anmeldung offen“, Fußzeilenköpfe) sowie Fokusring.
+- **Messing-Tinte** (#a55800): Orange als Text auf hellem Grund – Links wie „Alle News“, Status „Anmeldung offen“, Caret. Nie als Fläche.
+
+### Secondary
+- **Fördervereinsblau** (#78a8da): Markenfarbe des Fördervereins, ausschließlich für Förderverein-Inhalte reserviert: als volle Druckfläche eines Förderverein-Abschnitts (Tinte darauf, ≈ 6,8:1, auch im Nachtdruck unverändert blau), als Satzstrich im Seitenkopf der Förderverein-Seite und als Zeilenfüllung von Wegzeilen, die zum Förderverein führen. Nie als Textfarbe auf Papier (2,5:1). Auf der Startseite nicht im Einsatz.
+
+### Neutral
+- **Tinte** (#1c1d1f): Haupttext, Köpfe, 2px-Striche, primäre Schaltflächen, Fokusring auf Papier und Orange, Grund der Fußzeile und leeres Bildfeld.
+- **Papier** (#ffffff): Seitengrund und Text auf Tinte.
+- **Schiefer** (#58595b): Sekundärtext, Metadaten, inaktive Navigationslinks.
+- **Haarlinie** (#d4d4d1): 1px-Trennlinien zwischen Programm- und Registerzeilen, Lade-Platzhalter.
+- **Nachtgrund** (#141517), **Nacht erhöht** (#1d1f22), **Nachtlinie** (#34373c), **Nachtschrift** (#ecebe8), **Nacht gedämpft** (#a6a8ad): die Entsprechungen im Dunkelmodus für Grund, Menüs/Fußzeile/Bildfeld, Linien, Text und Sekundärtext.
+- **Abgesagt** (#b91c1c, Nacht #f87171): nur der Status „Abgesagt“ zusammen mit durchgestrichenem Titel.
+
+### Bezirksfarben
+- **Bezirk 01–13** (`district-1` … `district-13`): markieren ausschließlich einen Bezirk, als 10px-Quadrat vor dem Text „Bezirk NN · Name“. Termine ohne Bezirk tragen den Text „Bezirksübergreifend“ ohne Marke.
+
+### Named Rules
+**The Ink-on-Orange Rule.** Auf Orange steht immer Tinte (#1c1d1f), nie Weiß. Orange als Text auf hellem Grund ist immer Messing-Tinte; nur auf dem Nachtgrund darf Druckorange selbst Text sein.
+
+**The Quiet Open Rule.** Offene Anmeldung wird so leise markiert, dass die Seite auch dann nicht orange wird, wenn jeder Kurs offen ist: oranges Datumsfeld + Statuszeile „Anmeldung offen“ + Outline-Schaltfläche „Anmelden“; eine orange Hinterlegung nur für den Countdown ab 7 Tagen vor Fristende. Keine vollflächig orangen Zeilen.
+
+**The District Marker Rule.** Bezirksfarben sind Kennzeichen, keine Gestaltung: kleines Quadrat plus Bezirkstext, nie als Abschnittsfarbe, Fläche, Rahmen oder Schmuck.
+
+**The Förderverein Blue Rule.** Blau erscheint nur, wo der Förderverein spricht.
+
+**The One Field Rule.** Neben Titelblatt und Schlussaufruf trägt eine Seite höchstens eine weitere volle Farbfläche: die blaue Förderverein-Fläche, und nur für einen Abschnitt, in dem der Förderverein spricht. Eingeführt auf Wunsch des Eigentümers (2026-09-16), weil /mitmachen ohne Farbfläche „almost too minimalistic“ wirkte; Fotos lehnte er für diese Seite ab.
+
+## Typography
+
+**Display Font:** Archivo, variabel mit Breitenachse (mit ui-sans-serif, system-ui)
+**Body Font:** Archivo in normaler Breite
+**Label Font:** Archivo halbschmal (`wdth` 84)
+
+**Character:** Eine Familie, zwei Stimmen: die schmal-fette Stimme ruft wie ein Plakatkopf und echot die Logo-Wortmarke, die normale Breite liest sich ruhig und amtlich-freundlich. Datumsziffern sind tabellarisch, damit die Spalte steht.
+
+### Hierarchy
+- **Display** (800, clamp(2.75rem, 6vw, 5.75rem), 0.9, `wdth` 68, −0.01em): Titel auf dem Titelblatt.
+- **Headline** (800, clamp(2.25rem, 4.5vw, 3.5rem), 0.95, `wdth` 68): Abschnittsköpfe („Aktuelles“, Register); der Schlussaufruf nutzt dieselbe Stimme größer (clamp(2.5rem, 6vw, 5.25rem), 0.9).
+- **Programmkopf** (800, 2.25rem, 1, `wdth` 68): Kopf einer Programmspalte, mit 2px-Tintenstrich darunter.
+- **Listenkopf** (800, 1.75rem, 1, `wdth` 68): Kopf einer Registerliste; Fußzeilenköpfe gleiche Stimme in 1.5rem.
+- **Title** (700, 1.5rem, 1.1, `wdth` 68): Zeilentitel in Programm, Register und Wegzeilen (1.375–1.5rem); Beitragstitel 1.75rem / 1.05.
+- **Datumsziffer** (800, 2.75rem, 1, `wdth` 68, Tabellenziffern): Tag im Datumsslot; darunter der Monat halbschmal 600, 0.875rem, Versalien mit 0.06em.
+- **Lead** (500, clamp(1.25rem, 2.1vw, 1.875rem), snug, `wdth` 84, max. 26ch): Leitsatz unter dem Titel.
+- **Body** (400, 0.9375–1.125rem, 1.5–1.625, normale Breite): Zeit/Ort-Zeilen, Beschreibungen, Auszüge (max. 60ch).
+- **Meta** (600, 0.875rem, `wdth` 84): Art, Bezirk, Datum, Status – steht unter dem Titel.
+- **Button** (600, 1–1.125rem, `wdth` 84): Schaltflächen und Navigationslinks (1.0625rem).
+
+### Named Rules
+**The Two Widths Rule.** Titel, Daten und Köpfe sind schmal-fett; Etiketten und Schaltflächen halbschmal; Lesetext normal breit. Keine zweite Schriftfamilie auf den Programmheft-Flächen.
+
+**The Metadata Below Rule.** Keine Kicker- oder Eyebrow-Zeilen über Überschriften. Art, Bezirk, Datum und Status stehen unter dem Titel.
+
+**The Condensed Head Rule.** Abschnitts- und Listenköpfe sprechen in der schmal-extrafetten Stimme, nicht in gesperrten Versalien. Versalien mit Sperrung bleiben dem Monatskürzel im Datumsslot vorbehalten.
+
+## Layout
+
+Das Blatt (`sheet`) ist maximal 100rem breit, zentriert, mit Innenrand 1.25rem (mobil), 2.5rem (ab 40rem) und 3.5rem (ab 64rem). Größere Kompositionen folgen einem 12-Spalten-Raster ab 64rem: der erste Bildschirm teilt sich 7/12 Titelblatt und 5/12 Programmspalte und füllt mindestens min(56rem, Viewport minus Kopf); Register 8/4, Schlussaufruf 6/12 Kopf und 5/12 Text ab Spalte 8. Mobil stapelt sich alles einspaltig; das Titelblatt löst sich in Titel, dann Programm, dann Foto auf, damit offene Anmeldungen direkt unter dem Titel stehen.
+
+Innenseiten: Der Seitenkopf teilt sich 7/12 Titel und 5/12 Leitsatz (unten bündig); ein längerer Vorspann steht stattdessen in voller Zeilenlänge (max. 65ch) unter dem Titel. Abschnitte teilen sich ab 64rem 4/12 Kopf und Einleitung und 8/12 Inhalt (`Split`); aufeinanderfolgende Abschnitte wechseln die Seite des Kopfes wie linke und rechte Heftseiten (links, rechts, links …). Im DOM steht der Kopf immer zuerst, mobil über dem Inhalt; zu lange Wörter im 4/12-Kopf brechen an einem gesetzten weichen Trennstrich („Einstiegs-/möglichkeiten“), nie per automatischer Silbentrennung. Fließtext läuft höchstens 65ch; Tabellen, Hinweise, Merkpunkte und Weglisten füllen die ganze Inhaltsspalte, damit die rechte Kante steht. Eine Wegliste direkt unter dem Seitenkopf beginnt ohne eigene Luft und ohne zweiten Strich (`PageSection flush="top"`).
+
+Rhythmus: Abschnitte 4rem vertikal, ab 48rem 6rem. Zeilen 1rem vertikal mit 1–1.5rem Spaltenabstand; Wegzeilen mindestens 3.5rem hoch. Mehrspaltige Beitragslisten trennen Spalten mit 1px-Haarlinien und 2.5rem Innenabstand statt mit Lücken zwischen Kästen. Die Programmspalte zeigt im ersten Bildschirm höchstens drei Zeilen; ein fehlender Platz wird als gestalteter leerer Programmplatz (kurzer Strich + „Weitere Termine folgen.“) gesetzt. Klickziele sind mindestens 44px hoch.
+
+Bewegung: Zeilenfüllung 280ms `cubic-bezier(0.16, 1, 0.3, 1)` von links; Titelblatt-Foto überblendet in 800ms, wechselt alle 7s nur ohne Hover/Fokus, ist anhaltbar und steht bei `prefers-reduced-motion` still.
+
+## Elevation & Depth
+
+Das System ist vollständig flach. Es gibt keine Schlagschatten. Trennung entsteht durch Linien und Flächen: 2px Tinte unter Köpfen, über Weglisten, zwischen Abschnitten und unter der fixierten Navigation; Abschnittsstriche und der Abschluss des Seitenkopfs laufen vollbreit (Nacht: Nachtlinie), Kopf- und Gruppenstriche stehen im Satzspiegel (Nacht: Nachtschrift); 1px Haarlinien zwischen Zeilen; Farbflächen (Orange, Tinte) für Gewicht. Aufklappende Menüs liegen ohne Schatten auf dem Inhalt und grenzen sich mit einem 2px-Tintenrahmen ab (Nacht: 2px Nachtlinie auf Nacht erhöht). Die Navigationsunterkante ist technisch ein `inset`-Schatten von 2px ohne Unschärfe; er wirkt und zählt als Linie.
+
+### Named Rules
+**The Printed Depth Rule.** Tiefe kommt aus Linie, Fläche und Anschnitt, nie aus Licht. Braucht etwas Abstand zum Inhalt darunter, bekommt es einen 2px-Tintenrahmen, keinen Schatten.
+
+## Shapes
+
+Alles ist rechtwinklig (0px): Schaltflächen, Datumsfelder, Menüs, Bildfelder, Bezirksquadrate, Fokusringe. Einzige Ausnahme sind runde Avatar-Fotos bzw. Initialen im Benutzermenü. Linien kommen in zwei Stärken (2px Tinte, 1px Haarlinie); ein 6px × 96px Satzstrich trennt Titel und Leitsatz auf dem Titelblatt (Tinte auf Orange) und steht unter dem Vereinsnamen in der Fußzeile (Orange auf Tinte). Fotos laufen angeschnitten bis an die Kanten ihrer Fläche; Beitragsbilder stehen im Format 3:2, Titelblattfotos mobil 4:3. Fokus ist ein 3px-Umriss mit 2px Abstand (Tinte auf Papier und Orange, Druckorange auf Tinte und Nacht); ganze Zeilen zeigen den Fokus als 3px-Umriss nach innen.
+
+## Components
+
+### Buttons
+Kräftig und gedruckt, rechteckige Farbfelder ohne Rundung.
+- **Shape:** rechtwinklig (0px).
+- **Primary:** Tinte mit Papierschrift, halbschmal 600, 48px hoch, 24px seitlich, Pfeil rechts. Hover kehrt um auf Papier mit Tinte. Login in der Navigation: 44px hoch, Hover Orange mit Tinte; im Nachtdruck Orange mit Tinte, Hover Papier.
+- **Register (Outline):** 2px-Tintenrahmen, transparent, Tintenschrift, 40px hoch, 16px seitlich, Pfeil (extern: Pfeil schräg + Hinweis für Screenreader). Hover füllt Papier (sichtbar innerhalb der orange gefüllten Zeile). Nacht: Rahmen und Schrift Nachtschrift.
+- **Icon-Schaltflächen:** 44px Quadrat, transparent; Hover invertiert auf Tinte mit Papier (Nacht: Nachtschrift mit Nachtgrund). Auf dem Titelblatt dieselbe Umkehr für die Bildstrecken-Steuerung.
+- **Textlinks mit Pfeil:** halbschmal 600, Messing-Tinte (Nacht Orange), Unterstreichung mit 4px Abstand bei Hover.
+- **Primary auf Papier** (`ButtonLink`): Tinte mit Papierschrift, mindestens 48px hoch; Hover Orange mit Tinte. Im Nachtdruck Orange mit Tinte, Hover Papier. Auf Orange (`surface="orange"`) kehrt der Hover wie auf dem Titelblatt auf Papier um.
+- **Outline** (`ButtonLink variant="outline"`): 2px-Rahmen in Schriftfarbe, Hover füllt mit Schriftfarbe; auf Orange immer Tinte. Umrandet sind nur Aktionen, nie Etiketten.
+- **Textlink im Fließtext** (`.link-ink`): Messing-Tinte 600 (Nacht Orange), immer 1px unterstrichen mit 4px Abstand, Hover 2px; auf Orange und auf Druckflächen Tinte. Pfeil-Icons folgen dem Ziel: Pfeil intern, schräger Pfeil extern, Brief für E-Mail, Download für Dateien (mit Screenreader-Hinweis zu Dateityp und neuem Tab).
+
+### Programmzeile (Signature)
+Termine als Tabellensatz. Links der Datumsslot (64px, ab 40rem 80px): Tag als schmal-extrafette Tabellenziffer, darunter Monat (Jahr nur, wenn nicht das laufende). Rechts Titel (Title), darunter Meta-Zeile (Art · Bezirksmarke), Zeit · Ort in Body, dann Status. Ganz rechts ein Pfeil; die ganze Zeile ist per Link klickbar. 1px-Haarlinie unten.
+- **Offene Anmeldung:** Datumsslot als orange Fläche (8px Innenrand), Statuszeile „Anmeldung offen“ in Messing-Tinte, Frist als Text; ab 7 Tagen vor Ende orange hinterlegt mit Tinte; Plätze als Text; Outline-Schaltfläche „Anmelden“.
+- **Geschlossen / noch nicht offen:** Status in Schiefer, Datumsslot ohne Fläche.
+- **Abgesagt:** Titel durchgestrichen in Schiefer, Status „Abgesagt“ in Rot.
+- **Hover/Fokus:** Zeilenfüllung (siehe Wegzeile), alle Texte werden Tinte.
+
+### Wegzeile
+Wege als volle Zeile auf Papier: Title-Stimme, mindestens 56px hoch, Pfeil rechts, 1px-Haarlinie unten; Gruppen beginnen mit einem 2px-Tintenstrich. Bei Hover (nur Zeigegeräte) und Fokus füllt sich die Zeile in 280ms von links mit Druckorange, alles darauf wird Tinte. Registerzeilen sind Wegzeilen mit Beschreibung in Body unter dem Titel, zweispaltig ab 48rem.
+
+### Meldung (Beitragsspalte)
+Beiträge als Druckspalten, nicht als Karten: Bildfeld 3:2 auf Tinte (Nacht: Nacht erhöht), darunter Titel, Meta-Zeile (Datum, Kategorie, Bezirksmarke, „Angepinnt“) und Auszug (max. 4 Zeilen, 60ch). Spalten ab 64rem mit 1px-Haarlinie getrennt. Hover unterstreicht den Titel mit 3px Druckorange. Ohne Titelbild zeigt das Bildfeld das helle Logo auf Tinte, damit die Spalten gleich aufgebaut bleiben.
+
+### Titelblatt
+Volle orange Fläche mit Tintenschrift: Display-Titel, Satzstrich, Lead, Primary-Schaltfläche. Darüber dem Foto eine Zeile mit Bildnachweis und Bildstrecken-Steuerung (Zurück, „n / m“ in Tabellenziffern, Weiter, Anhalten); das Foto läuft bis an die Kanten.
+
+### Schlussaufruf
+Volle orange Fläche als bewusster Blickfang am Seitenende – vom Eigentümer ausdrücklich so gewünscht; nicht auf Papier verlegen. Headline-Stimme groß (clamp(2.5rem, 6vw, 5.25rem), 0.9) links (6/12), rechts ab Spalte 8 Text in Body (max. 40ch) und die Primary-Schaltfläche, z. B. „Mehr erfahren“; weitere Aktionen stehen als Outline daneben. Tinte auf Orange, im Nachtdruck unverändert Orange. Wiederverwendbar als `ClosingCall`, höchstens einmal pro Seite und immer als letzter Abschnitt vor der Fußzeile (Startseite: „Lust auf Posaunenchor?“, Mitmachen: „Noch Fragen?“).
+
+### Seitenkopf (Innenseite)
+`PublicPage` setzt jede Innenseite mit `PageHead` auf Papier: Brotkrumen halbschmal 600 in Schiefer (aktuelle Seite Tinte, Klickziele 44px, „/“ als Trenner), darunter genau ein `
` in Display-Größe (ab 44 Zeichen und bei kompakten Köpfen in Headline-Größe), ein 6px × 96px Satzstrich in Tinte (Nacht: Nachtschrift; Förderverein: Fördervereinsblau) und rechts der Leitsatz in der Lead-Stimme (clamp(1.25rem, 1.9vw, 1.625rem), max. 34ch). Abschluss ist ein vollbreiter 2px-Strich. Keine Kicker darüber, keine farbigen Kopfbänder, keine zweite klebende Titelleiste.
+- **Kompakt** (Kurs, Termin, Anmeldung, Ensemble): weniger Luft, Titel in Headline-Größe; ein abweichender Kurzname („Anmeldung“) steht halbschmal unter dem `
`. Rechts oben bündig die Meta-Spalte: Art, Dauer, Ort und Bezirksmarke als halbschmaler Schiefer-Text, Zustände als Etiketten, Aktionen („Bearbeiten“, „Teilen“) als 40px-Outline, darunter Zeilen mit Haarlinie für Datum, Ort, Plätze und Fristen mit Schiefer-Icons.
+
+### Abschnitt und Druckfläche
+`PageSection` ist ein Abschnitt auf Papier im Satzspiegel, 4rem/6rem vertikal, optional mit vollbreitem 2px-Strich darüber; `Split` setzt darin Kopf (4/12) und Inhalt (8/12) und wechselt mit `side="right"` die Seite des Kopfes. Mit `surface="foerderverein"` wird er zur vollen Druckfläche in Fördervereinsblau (`.print-field`): alle Schrift, Kopf- und Gruppenstriche in Tinte, Haarlinien Tinte 30 %, Wegzeilen füllen sich papierweiß, Fokus in Tinte, im Nachtdruck unverändert. Ein Satzstrich auf der Fläche steht in Tinte.
+
+### Merkpunkte
+`PointList` ersetzt Icon-Kacheln: Titel in der Title-Stimme, Text in Body-Schiefer darunter, 2px-Tintenstrich über der Liste, 1px-Haarlinien zwischen Zeilen und Spalten. Zwei Spalten ab 48rem für Gründe und Aufgaben, drei ab 40rem für kurze Fakten (z. B. „Nur 36 € / Jahr“ – ohne Großzahl-Inszenierung).
+
+### Beträge (Tabellensatz)
+`ValueTable` für Beiträge und Kennwerte: Bezeichnung links in Body 1.125rem, Wert rechtsbündig schmal-extrafett 1.75rem mit Tabellenziffern, Haarlinien zwischen den Zeilen, 2px-Tintenstrich darüber. Beträge nie in Orange.
+
+### Personenzeile
+`PersonRow` in `PersonList` (1–4 Spalten mit 2.5rem Abstand): rundes 56px-Foto (einzige Rundung neben dem Avatar), Name in der Title-Stimme (1.375rem), Amt in Body-Schiefer, „E-Mail senden“ als halbschmaler Textlink mit Brief-Icon (44px Klickziel, Name für Screenreader), Bildnachweis 0.875rem darunter; 1px-Haarlinie unten.
+
+### Etikett
+`Tag` nur für Zustände: gefüllt, rechteckig, halbschmal 600 0.875rem, keine Versalien, keine Sperrung. Töne: Tinte (auch auf Orange, z. B. „Beta“), invers (Tinte; Nacht Nachtschrift – „Vergangen“, „Ausgebucht“, „Demnächst“), Orange mit Tinte („Nur Warteliste“, „Mitspielen möglich!“) und Rot nur für „Abgesagt“.
+
+### Hinweis
+`Note` statt Alert-Box mit Seitenstreifen: `important` als orange Druckfläche mit Tinte für Fristen („Hinweis: … bis zum 30. November …“), `error` mit 2px-Rahmen und Kopf in Rot, `info` mit 2px-Tintenrahmen. Ein gewöhnlicher Zusatz (z. B. „Ehrungen“) ist kein Hinweis, sondern eine Gruppe mit Listenkopf.
+
+### Kasten
+`Panel` für einen Block, der sich vom Satz abheben muss (z. B. „Im Schadensfall“ mit Unterlagenliste und Schaltfläche): 2px-Tintenrahmen auf Papier, im Nachtdruck 2px Nachtschrift auf Nachtgrund, 1.5–2rem Innenrand. Höchstens ein Kasten pro Seite.
+### Navigation
+Fixierte Leiste auf Papier (Nacht: Nachtgrund), 64px / ab 64rem 80px hoch, 2px-Tintenlinie unten. Links halbschmal 600, 1.0625rem, Schiefer; Hover Tinte mit 3px-Tintenunterstrich; aktiv Tinte mit 3px-Orange-Unterstrich. Aufklappmenüs und Benutzermenü: Papier mit 2px-Tintenrahmen, Einträge 12px/16px, Hover Orange mit Tinte, aktueller Eintrag mit 8px-Quadrat in Tinte (Nacht Orange). Mobil: Vollbild-Liste unter der Leiste, Zeilen mit Haarlinien, 1.25rem halbschmal, Untermenüs über eine 64px breite Aufklappzelle mit Haarlinie links, Hover Orange.
+
+### Fußzeile
+Tintengrund (Nacht: Nacht erhöht) mit Papierschrift: Vereinsname in schmal-extrafetter Stimme, orangener Satzstrich, vier Spalten mit orangen Listenköpfen über einer hellen 1px-Linie (Papier 20 %), Links in Papier 85 %, Hover Orange mit Unterstreichung. Social-Icons als 44px-Quadrate, Hover Orange mit Tinte. Rechtszeile unter einer 1px-Linie (Papier 15 %) in 0.875rem.
+
+### Banner
+Volle Druckfläche über der Navigation, Info-Variante Orange mit Tinte, 0.875rem Text mit unterstrichenem Link, 44px Schließen-Schaltfläche, Hover 15 % Schriftfarbe. Das Etikett („Beta“) ist ein Tinten-Tag ohne Sperrung und Versalien. Warnung und Wartung stehen als Tintenfläche mit Papierschrift (Nacht: Nachtschrift-Fläche mit Nachtgrund-Schrift) und orangem Etikett; eine grüne Erfolgs-Variante gibt es nicht.
+
+## Do's and Don'ts
+
+### Do:
+- **Do** setze auf Orange immer Tinte (#1c1d1f) und nutze Messing-Tinte (#a55800) für orangen Text auf Papier; im Nachtdruck ist Druckorange Textfarbe.
+- **Do** markiere offene Anmeldung mit orangem Datumsfeld, Statuszeile „Anmeldung offen“ und Outline-Schaltfläche „Anmelden“; orange Hinterlegung nur für den Countdown ab 7 Tagen.
+- **Do** kennzeichne Bezirke mit 10px-Quadrat plus „Bezirk NN · Name“ und schreibe „Bezirksübergreifend“ als Text.
+- **Do** trenne mit Linien: 2px Tinte unter Köpfen und über Weglisten, 1px Haarlinie zwischen Zeilen.
+- **Do** setze Wege als volle Wegzeilen auf Papier mit Pfeil und Orange-Füllung bei Hover/Fokus; der Schlussaufruf „Lust auf Posaunenchor?“ steht bewusst auf voller oranger Fläche (Blickfang, vom Eigentümer gewünscht).
+- **Do** zeige Beiträge ohne Titelbild mit dem hellen Logo auf dem Tinten-Bildfeld.
+- **Do** halte WCAG 2.1 AA in Hell- und Dunkelmodus ein: Kontrast ≥ 4,5:1, 44px Klickziele, sichtbarer 3px-Fokus, anhaltbare Bildstrecke und `prefers-reduced-motion`.
+- **Do** reserviere Fördervereinsblau (#78a8da) für Förderverein-Inhalte; wo der Förderverein einen ganzen Abschnitt spricht, darf er eine volle blaue Druckfläche mit Tinte bekommen.
+- **Do** setze jede Innenseite mit `PublicPage`: Papierkopf, genau ein `
`, vollbreiter Abschlussstrich.
+- **Do** setze Art, Dauer, Ort und Bezirk als Meta-Text; Etiketten nur für Zustände, Rahmen nur für Aktionen.
+- **Do** beende eine Seite höchstens mit einem Schlussaufruf, und dann als letzten Abschnitt.
+
+### Don't:
+- **Don't** Karten, Schlagschatten oder Rundungen verwenden (Ausnahme: runde Avatar-Fotos); schwebende Menüs trennen sich mit 2px-Tintenrahmen.
+- **Don't** Icon-Kreise oder farbige Seiten- oder Kopfstreifen einsetzen; volle orange Flächen nur als bewusster Blickfang (Titelblatt, Schlussaufruf), nicht als Dekor für beliebige Abschnitte; dazu höchstens eine blaue Förderverein-Fläche pro Seite (One Field Rule).
+- **Don't** ganze Zeilen dauerhaft orange füllen, um offene Anmeldungen zu markieren – wären alle Kurse offen, wäre die Seite orange.
+- **Don't** Bezirksfarben als Dekoration, Fläche oder Abschnittsfarbe nutzen.
+- **Don't** Kicker- oder Eyebrow-Labels über Überschriften setzen; Metadaten gehören unter den Titel.
+- **Don't** Abschnitts- oder Listenköpfe in gesperrten Versalien setzen; sie sprechen schmal-extrafett.
+- **Don't** Weiß auf Orange oder Druckorange als Text auf Papier setzen.
+- **Don't** Seitenköpfe in Bezirksfarben oder Orange einfärben oder eine zweite Titelleiste einblenden.
+- **Don't** mehr als einen Kasten pro Seite setzen oder Etiketten wie Schaltflächen umranden.
+- **Don't** Fördervereinsblau als Text auf Papier oder für Inhalte außerhalb des Fördervereins verwenden.
diff --git a/Dockerfile b/Dockerfile
index e99a4640..6473fa37 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,3 @@
-# ================================
-# Build stage
-# ================================
FROM node:24-alpine AS builder
# Install dependencies needed for native modules
@@ -23,14 +20,11 @@ COPY prisma.config.ts ./
ARG DATABASE_URL
ENV DATABASE_URL=${DATABASE_URL}
-# Install all dependencies (including devDependencies for build)
-# This will run prisma generate via postinstall hook
+# postinstall runs prisma generate
RUN pnpm install --frozen-lockfile
-# Copy source code
COPY . .
-# Build the application with memory optimizations
ENV SKIP_ENV_VALIDATION=1
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
@@ -38,41 +32,29 @@ ENV NODE_OPTIONS="--max-old-space-size=2048"
RUN pnpm build
-# Drop devDependencies before the runner copies node_modules — the runtime
-# entrypoints (next, prisma migrate, tsx for seeds) are all regular
-# dependencies, so the image doesn't need typescript/eslint/tailwind etc.
+# Runtime entrypoints (next, prisma migrate, tsx) are regular dependencies.
RUN pnpm prune --prod
-# ================================
-# Production stage
-# ================================
FROM node:24-alpine AS runner
-# Install dependencies needed for runtime
RUN apk add --no-cache libc6-compat
WORKDIR /app
-# Create non-root user for security and give it ownership of the workdir.
-# /app itself must be writable by the runtime user (next may write to cwd
-# in some cases) — the COPY --chown lines below only chown the files
-# copied in, not the /app directory entry itself.
+# /app itself must be writable by nextjs (next may write to cwd); COPY --chown
+# only chowns the copied files, not the directory.
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 --ingroup nodejs nextjs \
&& chown nextjs:nodejs /app \
- # Uploads volume mount point (outside public/ so access goes through the
- # authorizing /api/uploads route)
+ # Uploads outside public/, so access goes through the authorizing /api/uploads route
&& mkdir -p /app/uploads \
&& chown nextjs:nodejs /app/uploads
-# Set production environment
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
-# We do NOT install corepack/pnpm in the runner. All runtime entrypoints
-# (next, prisma, tsx) are invoked directly via node_modules/.bin to avoid
-# pnpm 11's runDepsStatusCheck, which tries to wipe and reinstall
-# node_modules on every script invocation and fails in containers (no TTY).
+# No pnpm in the runner: pnpm 11's runDepsStatusCheck tries to reinstall node_modules
+# on every script run and fails in containers (no TTY). Entrypoints run via node directly.
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./
# Copy the full build output (non-standalone)
@@ -84,17 +66,10 @@ COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
COPY --from=builder --chown=nextjs:nodejs /app/prisma.config.ts ./
-# Copy generated Prisma client
COPY --from=builder --chown=nextjs:nodejs /app/generated ./generated
-# Copy tsconfig + the minimal src needed by the prisma/*.ts scripts that run
-# with tsx in the container (post-migration-setup.ts, backfill-slugs.ts,
-# backfill-phone-format.ts, per the app command in deploy/stack.yaml).
-# Everything those scripts import has to be listed here, transitively: the
-# rest of src/ is not in the runtime image, so a file missing from this list
-# still builds fine and only fails when the script runs at container start,
-# where the failure is non-fatal and therefore quiet. db.ts is imported by
-# all three, so a gap in its own imports takes every one of them down.
+# Minimal src for the prisma/*.ts scripts run with tsx at container start (deploy/stack.yaml).
+# List every transitive import: a missing file builds fine and fails only at runtime, quietly.
COPY --from=builder --chown=nextjs:nodejs /app/tsconfig.json ./
COPY --from=builder --chown=nextjs:nodejs /app/src/server/db.ts ./src/server/
COPY --from=builder --chown=nextjs:nodejs /app/src/server/utils/logger.ts ./src/server/utils/
@@ -102,22 +77,21 @@ COPY --from=builder --chown=nextjs:nodejs /app/src/lib/permissions.ts ./src/lib/
COPY --from=builder --chown=nextjs:nodejs /app/src/lib/bezirke.ts ./src/lib/
COPY --from=builder --chown=nextjs:nodejs /app/src/lib/slug.ts ./src/lib/
COPY --from=builder --chown=nextjs:nodejs /app/src/lib/phone-number.ts ./src/lib/
+COPY --from=builder --chown=nextjs:nodejs /app/src/lib/description-linebreaks.ts ./src/lib/
-# Trigger script for the mStudio cron job (mittwald), see the script header
+# Trigger scripts for the mStudio cron jobs (mittwald), see the script headers
COPY --chown=nextjs:nodejs scripts/trigger-registration-closed.mjs ./scripts/
+COPY --chown=nextjs:nodejs scripts/trigger-newsletter-cleanup.mjs ./scripts/
+COPY --chown=nextjs:nodejs scripts/trigger-waitlist-offers.mjs ./scripts/
-# Switch to non-root user
USER nextjs
-# Expose port
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
-# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1
-# Start the application using next directly (no pnpm wrapper, see comment
-# above about runDepsStatusCheck).
+# No pnpm wrapper, see runDepsStatusCheck above.
CMD ["node", "node_modules/next/dist/bin/next", "start"]
\ No newline at end of file
diff --git a/PRODUCT.md b/PRODUCT.md
new file mode 100644
index 00000000..8f9e4a33
--- /dev/null
+++ b/PRODUCT.md
@@ -0,0 +1,87 @@
+# Product
+
+
+
+## Platform
+
+web
+
+(Installierbare PWA mit handgeschriebenem Service Worker und Offline-Seite; Spiele laufen im Standalone-Modus mit Safe-Area-Unterstützung. Es gibt keine native App.)
+
+## Users
+
+**Hauptzielgruppe: aktive Chormitglieder.** Bläserinnen und Bläser sowie Chorleitungen aus den Posaunenchören der Evangelischen Kirche im Rheinland (13 Bezirke). Sie kommen mit einer konkreten Aufgabe: einen Termin nachsehen, sich zu einem Lehrgang anmelden (inkl. Anzahlung und Rechnung), die eigene Anmeldung verwalten, Materialien und Noten finden, das Rheinische Blechblatt lesen oder mit den Spielen üben. Viele nutzen die Seite unterwegs auf dem Handy, und die Altersspanne reicht von Jungbläser:innen bis zu langjährigen, älteren Mitgliedern.
+
+Weitere bestätigte Zielgruppen, bei Designentscheidungen nachrangig:
+
+- **Neue und Interessierte** (Eltern, Kinder und Jugendliche, Wiedereinsteiger:innen), die über „Mitmachen“ einen Chor in der Nähe, die Jungbläserarbeit oder Bildungsangebote finden.
+- **Das Dashboard-Team:** Landesposaunenwarte, Regionalposaunenwarte, die Geschäftsstelle, Obleute und Ehrenamtliche. Sie pflegen Termine, Kurse, Beiträge, Ensembles, Personen, Newsletter, Rechnungen und Anmeldungen und geben Inhalte frei.
+- **Förderer:** Mitglieder und mögliche Mitglieder des Fördervereins Rheinisches Posaunenwerk.
+
+## Product Purpose
+
+Die Plattform ist das Zuhause der evangelischen Bläserarbeit im Rheinland. Sie verbindet den öffentlichen Auftritt des Posaunenwerks (Termine, Lehrgänge, Aktuelles, Materialien, Struktur, Prävention, Förderverein) mit den Werkzeugen, mit denen der Verband seine Arbeit organisiert.
+
+Erfolg bemisst sich an vier gleichrangigen Zielen:
+
+1. **Lehrgangsanmeldungen:** mehr und reibungslosere Anmeldungen zu Kursen und Lehrgängen, von der Anmeldung über die Anzahlung bis zur Rechnung.
+2. **Nachwuchs und Chorsuche:** mehr Menschen finden einen Posaunenchor und steigen in die Bläserarbeit ein, besonders Kinder und Jugendliche.
+3. **Weniger Verwaltungsaufwand:** Geschäftsstelle und Ehrenamtliche sparen Zeit beim Pflegen, Freigeben und Kommunizieren.
+4. **Sichtbarkeit und Bindung:** Aktuelles, Newsletter, Blechblatt, Spiele und Förderverein halten die Gemeinschaft über die Bezirke hinweg zusammen.
+
+## Positioning
+
+Die offizielle Plattform des Posaunenwerks der Evangelischen Kirche im Rheinland e.V. Andere Musikseiten können das nicht glaubwürdig nachahmen: Termine, Lehrgänge, Chorverzeichnis, Bezirksstruktur, Materialien des Verbands und die Lern-Spiele für Blechbläser kommen aus einer Hand und aus derselben Datenbank, gepflegt von den Menschen, die die Bläserarbeit tatsächlich tragen. Die Plattform ist zugleich Schaufenster und Arbeitsmittel des Verbands. Leitmotiv laut Startseite: „Gemeinsam Musik machen, Glauben leben“.
+
+## Operating Context
+
+- **Öffentliche Seite (Deutsch, `lang="de"`):** Start, Termine, Aktuelles, Mitmachen (Chor finden mit Karte, Aus- und Weiterbildung, Jungbläserarbeit, Ehrenamt), Materialien (Rheinisches Blechblatt, Literatur & CDs), Spiele, Über uns (Struktur & Geschichte, Vorstand, Posaunenwarte, Bezirke & Obleute, Auswahlchöre, Posaunenrat), Förderverein, Prävention, Newsletter, Suche, Kontakt, Impressum, Datenschutz, Lizenzen.
+- **Konto-Bereich:** Registrierung, Login mit Zwei-Faktor-Authentifizierung, eigene Anmeldungen, Anmeldung per Link verwalten (`/anmeldung-verwalten`), Einstellungen.
+- **Dashboard:** Inhaltspflege für alle Entitäten, Freigabe-Workflow (DRAFT → PENDING → APPROVED), Kursverwaltung mit Teilnehmenden, individuellen Anmeldefeldern, Preisoptionen, Anzahlungen, Rechnungen und Kurs-Mails, Newsletter, Medienbibliothek mit Freigabe, Startseiten-Karussell, Statistik, Audit-Log, Rollen und Rechte (hierarchische Rollen, Bezirks-Zuständigkeiten), Export/Import, Wartungsmodus, Social-Media-Export für Instagram.
+- **Spiele:** Rhythmus, Noten lesen, Griffe (Ventile/Zugpositionen) und Notenwaage, kostenlos im Browser und offline nutzbar. Die Statistik liegt zuerst lokal und wird bei Anmeldung synchronisiert. Öffentliche Notensets gibt es auch.
+- **Rituale und Kanäle:** Lehrgangssaison mit Anmeldefristen und Anzahlungen, Fördervereinstag, Rheinisches Blechblatt als Magazin, Newsletter mit Double-Opt-in, RSS-Feed für Aktuelles, Social Media (Facebook, Instagram, YouTube).
+- **Umgebungen:** Die Produktion läuft auf mittwald. Eine Vorab-Umgebung zeigt ein Beta-Banner (`APP_ENV` ≠ `production`), auf ihr ist die Feedback-Seite aktiv.
+
+## Capabilities and Constraints
+
+- **Stack (bestehend):** Next.js 16 (App Router, Turbopack), TypeScript, tRPC, Prisma/PostgreSQL, Better Auth, Tailwind CSS 4, React Query. Tiptap für Rich-Text, Leaflet für Karten, Recharts für Statistiken, VexFlow (nur `vexflow/bravura`) für Notensatz.
+- **Hell- und Dunkelmodus** werden beide unterstützt (Systemeinstellung oder manuelle Wahl).
+- **DSGVO:** Tracking nur mit Einwilligung, Datenexport und Löschwege für Admins (siehe `docs/DSGVO-COMPLIANCE.md`).
+- **Kinderschutz** ist Pflichtthema: Präventionsseite, Selbstverpflichtung und Zusatzerklärung für Minderjährige.
+- **Rollen:** Landesposaunenwart (LPW) gibt alles frei, Regionalposaunenwart (RPW) die zugewiesenen Bezirke, Obleute erstellen Inhalte, die freigegeben werden müssen. Die Rechte sind feingranular und hierarchisch.
+- **Fachbegriffe beibehalten:** Posaunenchor, Bläser:innen, Jungbläser, Lehrgang, Bezirk, Obleute, Posaunenwart (LPW/RPW), Posaunenrat, Auswahlchor, Bläserheft, Blechblatt, Förderverein.
+- **Offen:** Die Anrede ist nicht festgelegt. Öffentliche Texte duzen meist („Finde einen Posaunenchor …“), einzelne Stellen siezen („für Ihren Posaunenchor“). Das ist noch zu entscheiden.
+
+## Brand Commitments
+
+Verbindlich sind nur diese Vorgaben, alles andere am Erscheinungsbild ist frei:
+
+- **Logo:** `public/images/logo-horizontal.svg`, `logo-horizontal-dark.svg`, `logo-icon.svg`, `logo-icon-dark.svg`, `logo.png`, dazu App-Icons in `public/` (inkl. maskable).
+- **Primärfarbe:** `#faa619` (Token `--color-primary` in `src/styles/globals.css`).
+- **Fördervereinsfarbe:** `#78a8da` (Token `--color-foerderverein`). Sie kennzeichnet den Förderverein.
+- **Name:** „Posaunenwerk Rheinland“. Die rechtliche Bezeichnung lautet „Posaunenwerk der Evangelischen Kirche im Rheinland e.V.“, der Förderverein heißt „Förderverein Rheinisches Posaunenwerk“.
+
+## Evidence on Hand
+
+- **Echte Organisationsdaten:** Anschrift, Telefon, E-Mail und Social-Media-Profile in `src/lib/structured-data.ts` (synchron mit dem Impressum).
+- **Offizielle Dokumente** in `public/downloads/`: Satzung, Ehrenordnung, Aufnahmeantrag für Chöre, Arbeitshilfe Jungbläser, Leistungsstempel, Förderverein-Flyer, Selbstverpflichtung Kinderschutz, Zusatzerklärung Minderjährige.
+- **Bilder** von Personen und Gremien in `public/images/` (team, vorstand, posaunenwarte, bezirke, auswahlchoere, blaeserhefte). Hinzu kommen hochgeladene Medien in der Datenbank mit Bildnachweisen.
+- **Audio:** `public/audio/frankreich-2021-sample.mp3`.
+- **Live-Inhalte:** Termine, Lehrgänge, Beiträge, Chöre, Bezirke und Personen stehen in der Datenbank.
+- **Bestätigte Kennzahlen** (Wortlaut auf den Seiten, vom Nutzer als Inhalt bestätigt): annähernd 200 Posaunenchöre, über 2.000 aktive Bläserinnen und Bläser, über 140 Jahre Posaunenchorarbeit im Rheinland, 13 Bezirke, Förderverein seit 2008 (`src/app/ueber-uns/struktur/page.tsx`, `src/app/mitmachen/page.tsx`, `src/app/mitmachen/chor-finden/page.tsx`).
+- **Fotos:** Lokal gibt es nicht genug echte Fotos für eine bildgeführte Gestaltung (Medienbibliothek der Dev-Umgebung plus Porträts in `public/images/`). Der Nutzer liefert Fotos nach, braucht dafür aber genaue Beschreibungen, was gewollt ist (Motiv, Ausschnitt, Format, Einsatzort).
+- **Nicht vorhanden, darf nicht erfunden werden:** Testimonials, Wirkungsstatistiken und Zitate. `news-placeholder-*.jpg` und `profile-placeholder.jpg` sind Platzhalter, keine echten Fotos.
+
+## Product Principles
+
+1. **Chormitglieder zuerst.** Wer schon dabei ist, soll Termin, Lehrgang, Anmeldung oder Material ohne Umweg finden. Diese Aufgaben haben Vorrang vor Selbstdarstellung.
+2. **Anmelden muss leicht sein.** Jeder Schritt vom Lehrgang zur bestätigten Anmeldung (samt Anzahlung und Rechnung) muss auch für ungeübte und ältere Menschen auf dem Handy klar und fehlertolerant sein.
+3. **Die Tür für Neue offen halten.** Kinder, Eltern und Wiedereinsteiger:innen verstehen die Fachsprache des Verbands nicht automatisch. Einstiege erklären statt voraussetzen.
+4. **Ehrenamt entlasten.** Jede Dashboard-Funktion soll Aufwand sparen und Fehler verhindern, denn die Pflegenden haben wenig Zeit und unterschiedliche technische Erfahrung.
+5. **Eine Gemeinschaft, viele Bezirke.** Die Bezirke, Chöre und Menschen sollen sichtbar sein. Die Plattform verbindet, statt nur zu verwalten.
+
+## Accessibility & Inclusion
+
+- **Pflicht:** Barrierefreiheit nach WCAG bzw. BITV. Die genaue Konformitätsstufe ist nicht benannt. BITV 2.0 verweist über EN 301 549 auf WCAG 2.1 AA, das gilt deshalb bis zur Klärung als Mindestmaß.
+- Die Nutzenden sind von Kindern bis zu Senior:innen sehr gemischt. Darum sind gut lesbare Schrift, ausreichende Kontraste in Hell- und Dunkelmodus, große Touch-Ziele und klare Fehlermeldungen wichtig.
+- Die Spiele setzen auf Hören, Timing und Notenlesen. Sie brauchen Alternativen, wo das möglich ist (z. B. visuelles Metronom), und müssen reduzierte Bewegung respektieren.
diff --git a/deploy/stack.yaml b/deploy/stack.yaml
index d9aec75c..82270bc7 100644
--- a/deploy/stack.yaml
+++ b/deploy/stack.yaml
@@ -30,6 +30,7 @@ services:
node node_modules/tsx/dist/cli.mjs prisma/post-migration-setup.ts || echo "Post-migration setup failed (non-fatal)";
node node_modules/tsx/dist/cli.mjs prisma/backfill-slugs.ts || echo "Slug backfill failed (non-fatal)";
node node_modules/tsx/dist/cli.mjs prisma/backfill-phone-format.ts || echo "Phone format backfill failed (non-fatal)";
+ node node_modules/tsx/dist/cli.mjs prisma/backfill-description-linebreaks.ts || echo "Description line break backfill failed (non-fatal)";
exec node node_modules/next/dist/bin/next start
volumes:
- "uploads_data:/app/uploads"
@@ -39,9 +40,8 @@ services:
# Öffentliche Seite: blendet u. a. das Beta-Banner aus.
APP_ENV: "production"
NEXT_TELEMETRY_DISABLED: "1"
- # Log-Ausführlichkeit: debug | info | warn | error | silent.
- # "warn" hält das tRPC-Rauschen aus dem Container-Log, damit Fehler
- # lesbar bleiben. Zum Debuggen eines Live-Problems auf "debug" setzen.
+ # debug | info | warn | error | silent. "warn" hält das tRPC-Rauschen aus dem Log;
+ # für ein Live-Problem auf "debug" setzen.
LOG_LEVEL: "warn"
NEXT_PUBLIC_APP_URL: "{{ .Env.APP_URL }}"
BETTER_AUTH_SECRET: "{{ .Env.BETTER_AUTH_SECRET }}"
@@ -53,9 +53,8 @@ services:
SMTP_PASSWORD: "{{ .Env.SMTP_PASSWORD }}"
SMTP_FROM: "{{ .Env.SMTP_FROM }}"
CRON_SECRET: "{{ .Env.CRON_SECRET }}"
- # Wartungsmodus. Leer = aus; der Alltagsschalter sitzt im Dashboard.
- # Auf "true" setzen, um die Seite unabhaengig von der Datenbank zu
- # schliessen (Deployment-Fenster) — greift auch waehrend einer Migration.
+ # Leer = aus (Alltagsschalter im Dashboard). "true" schließt die Seite unabhängig
+ # von der Datenbank, auch während einer Migration.
MAINTENANCE_MODE: "{{ .Env.MAINTENANCE_MODE }}"
MAINTENANCE_BYPASS_TOKEN: "{{ .Env.MAINTENANCE_BYPASS_TOKEN }}"
ADMIN_EMAIL: "{{ .Env.ADMIN_EMAIL }}"
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index 924ec31d..3a793672 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -1,24 +1,7 @@
-# =============================================================================
-# Posaunenwerk Docker Compose - Production (uses pre-built image)
-# =============================================================================
-#
-# This file is for production deployment using pre-built images from GHCR.
-# For local development, use docker-compose.yml instead.
-#
-# Quick Start:
-# 1. Copy .env.example to .env and configure values
-# 2. Set CRON_SECRET (and SMTP_*) in .env
-# 3. Run: docker compose -f docker-compose.prod.yml up -d
-#
-# APP_IMAGE_TAG pins the image version (default: latest). The release
-# workflow sets it to the exact release version so pre-releases deploy too.
-#
-# =============================================================================
+# Server deployment with pre-built GHCR images; local development uses docker-compose.yml.
+# APP_IMAGE_TAG pins the version (default latest); the release workflow sets it so pre-releases deploy too.
services:
- # ===========================================================================
- # PostgreSQL Database
- # ===========================================================================
posaunenwerk-db:
image: postgres:16-alpine
container_name: posaunenwerk-db
@@ -42,9 +25,6 @@ services:
retries: 5
start_period: 10s
- # ===========================================================================
- # Database Migration (runs once on startup)
- # ===========================================================================
db-migrate:
image: ghcr.io/nichtlehdev/pwr-web:${APP_IMAGE_TAG:-latest}
container_name: posaunenwerk-migrate
@@ -57,14 +37,8 @@ services:
DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@posaunenwerk-db:5432/${POSTGRES_DB:-posaunenwerk}
NODE_ENV: production
SKIP_ENV_VALIDATION: 1
- # Invoke prisma directly (not via pnpm) to avoid pnpm 11's
- # runDepsStatusCheck which tries to reinstall node_modules at startup.
- #
- # The slug backfill runs straight after the migration, mirroring the app
- # start command in deploy/stack.yaml, so a deploy never leaves new rows
- # without a slug. It is non-fatal on purpose: slugs are nullable and the
- # detail routes fall back to the UUID, so a failure here must not stop
- # `app` — which waits on this service — from coming up.
+ # prisma directly, not via pnpm (pnpm 11's runDepsStatusCheck reinstalls node_modules).
+ # Slug backfill is non-fatal: slugs are nullable with UUID fallback, and `app` waits on this.
command:
- "sh"
- "-c"
@@ -74,9 +48,7 @@ services:
echo "Slug backfill failed (non-fatal)"; }
restart: "no"
- # ===========================================================================
- # Post-Migration Setup (run once after permission migrations)
- # ===========================================================================
+ # Run once after permission migrations
post-migration-setup:
image: ghcr.io/nichtlehdev/pwr-web:${APP_IMAGE_TAG:-latest}
container_name: posaunenwerk-post-migration-setup
@@ -97,16 +69,8 @@ services:
["node", "node_modules/tsx/dist/cli.mjs", "prisma/post-migration-setup.ts"]
restart: "no"
- # ===========================================================================
- # Slug Backfill (fills Post/Ensemble/Event/Course slugs)
- #
- # Runs automatically as part of db-migrate; this profile is for re-running it
- # on demand — imports and duplicates deliberately leave the slug NULL, so new
- # rows can appear without one between deploys.
- #
- # Idempotent: only rows whose slug is still NULL are touched.
+ # Re-run on demand (imports and duplicates leave the slug NULL); only touches NULL slugs:
# docker compose -f docker-compose.prod.yml --profile slug-backfill up
- # ===========================================================================
slug-backfill:
image: ghcr.io/nichtlehdev/pwr-web:${APP_IMAGE_TAG:-latest}
container_name: posaunenwerk-slug-backfill
@@ -126,9 +90,6 @@ services:
command: ["node", "node_modules/tsx/dist/cli.mjs", "prisma/backfill-slugs.ts"]
restart: "no"
- # ===========================================================================
- # Database Seeding (optional, run manually)
- # ===========================================================================
db-seed:
image: ghcr.io/nichtlehdev/pwr-web:${APP_IMAGE_TAG:-latest}
container_name: posaunenwerk-seed
@@ -150,9 +111,6 @@ services:
command: ["node", "node_modules/tsx/dist/cli.mjs", "prisma/seed.ts"]
restart: "no"
- # ===========================================================================
- # Database Backup Service (scheduled backups)
- # ===========================================================================
db-backup:
image: ghcr.io/nichtlehdev/pwr-backup:${APP_IMAGE_TAG:-latest}
container_name: posaunenwerk-backup
@@ -175,9 +133,6 @@ services:
BACKUP_SCHEDULE: ${BACKUP_SCHEDULE:-0 2 * * *} # Default: 2 AM daily
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-30} # Keep backups for 30 days
- # ===========================================================================
- # Registration-closed e-mails (every 6h, calls app cron API)
- # ===========================================================================
registration-closed-cron:
image: ghcr.io/nichtlehdev/pwr-cron:${APP_IMAGE_TAG:-latest}
container_name: posaunenwerk-registration-closed-cron
@@ -193,9 +148,6 @@ services:
APP_INTERNAL_URL: http://app:3000
REGISTRATION_CLOSED_CRON_SCHEDULE: ${REGISTRATION_CLOSED_CRON_SCHEDULE:-0 */6 * * *}
- # ===========================================================================
- # Next.js Application
- # ===========================================================================
app:
image: ghcr.io/nichtlehdev/pwr-web:${APP_IMAGE_TAG:-latest}
container_name: posaunenwerk-app
@@ -211,31 +163,23 @@ services:
expose:
- "3000"
volumes:
- # Mounted outside public/ so file access always goes through the
- # authorizing /api/uploads route (public/ would be served directly).
+ # Outside public/, so access goes through the authorizing /api/uploads route.
- uploads_data:/app/uploads
environment:
- # Database
DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@posaunenwerk-db:5432/${POSTGRES_DB:-posaunenwerk}
- # Application
NODE_ENV: production
NEXT_TELEMETRY_DISABLED: 1
- # Log verbosity: debug | info | warn | error | silent.
- # Production defaults to warn so per-request tRPC chatter stays out of
- # the container log and errors are actually readable. Set to debug for
- # one deploy when tracing a live issue.
+ # debug | info | warn | error | silent. "warn" keeps tRPC chatter out of the log;
+ # set to debug when tracing a live issue.
LOG_LEVEL: ${LOG_LEVEL:-warn}
- # Public URLs
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
- # Better Auth
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?BETTER_AUTH_SECRET is required}
BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000}
- # Github Settings
GITHUB_REPO: ${GITHUB_REPO:-}
GITHUB_TOKEN: ${GITHUB_TOKEN:-}
diff --git a/docker-compose.yml b/docker-compose.yml
index 209a9e51..3d80e782 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,20 +1,7 @@
-# =============================================================================
-# Posaunenwerk Docker Compose Configuration
-# =============================================================================
-#
-# Quick Start:
-# 1. Copy .env.example to .env and configure values
-# 2. Update DATABASE_URL in .env to use @posaunenwerk-db:5432
-# 3. Run: docker compose up -d
-# 4. Set CRON_SECRET in .env (required for registration-closed-cron)
-# 5. (Optional) Seed database: docker compose --profile seed run --rm db-seed
-#
-# =============================================================================
+# DATABASE_URL in .env must use @posaunenwerk-db:5432; CRON_SECRET is required.
+# Seed: docker compose --profile seed run --rm db-seed
services:
- # ===========================================================================
- # PostgreSQL Database
- # ===========================================================================
posaunenwerk-db:
image: postgres:16-alpine
container_name: posaunenwerk-db
@@ -38,9 +25,6 @@ services:
retries: 5
start_period: 10s
- # ===========================================================================
- # Database Migration (runs once on startup)
- # ===========================================================================
db-migrate:
build:
context: .
@@ -61,9 +45,6 @@ services:
["node", "node_modules/prisma/build/index.js", "migrate", "deploy"]
restart: "no"
- # ===========================================================================
- # Database Seeding (optional, run manually)
- # ===========================================================================
db-seed:
build:
context: .
@@ -90,9 +71,6 @@ services:
command: ["node", "node_modules/tsx/dist/cli.mjs", "prisma/seed.ts"]
restart: "no"
- # ===========================================================================
- # Database Backup Service (scheduled backups)
- # ===========================================================================
db-backup:
build:
context: .
@@ -117,9 +95,6 @@ services:
BACKUP_SCHEDULE: ${BACKUP_SCHEDULE:-0 2 * * *} # Default: 2 AM daily
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-30} # Keep backups for 30 days
- # ===========================================================================
- # Registration-closed e-mails (every 6h, calls app cron API)
- # ===========================================================================
registration-closed-cron:
build:
context: .
@@ -137,9 +112,6 @@ services:
APP_INTERNAL_URL: http://app:3000
REGISTRATION_CLOSED_CRON_SCHEDULE: ${REGISTRATION_CLOSED_CRON_SCHEDULE:-0 */6 * * *}
- # ===========================================================================
- # Next.js Application
- # ===========================================================================
app:
build:
context: .
@@ -160,23 +132,19 @@ services:
expose:
- "3000"
volumes:
- # Mounted outside public/ so file access always goes through the
- # authorizing /api/uploads route (public/ would be served directly).
+ # Outside public/, so access goes through the authorizing /api/uploads route.
- uploads_data:/app/uploads
environment:
- # Database - override any build-time URL with runtime URL
+ # Overrides the build-time URL
DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@posaunenwerk-db:5432/${POSTGRES_DB:-posaunenwerk}
GITHUB_REPO: ${GITHUB_REPO:-}
GITHUB_TOKEN: ${GITHUB_TOKEN:-}
- # Application
NODE_ENV: production
NEXT_TELEMETRY_DISABLED: 1
- # Public URLs
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
- # Better Auth
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?BETTER_AUTH_SECRET is required}
BETTER_AUTH_URL: ${BETTER_AUTH_URL:-http://localhost:3000}
diff --git a/docs/DEPLOYMENT-CHECKLIST.md b/docs/DEPLOYMENT-CHECKLIST.md
index cef01f44..01476bd8 100644
--- a/docs/DEPLOYMENT-CHECKLIST.md
+++ b/docs/DEPLOYMENT-CHECKLIST.md
@@ -145,15 +145,21 @@ Reihenfolge beim ersten Deploy auf eine leere Datenbank:
Die geplanten Jobs laufen auf mittwald **nicht** im Stack, sondern als
mStudio-Cronjobs. Sie müssen dort einmalig angelegt sein — ohne sie werden
-keine Anmeldeschluss-Mails verschickt und unbestätigte Newsletter-Anmeldungen
-nie gelöscht (was die Datenschutzerklärung mit 30 Tagen zusagt).
+keine Anmeldeschluss-Mails verschickt, unbestätigte Newsletter-Anmeldungen
+nie gelöscht (was die Datenschutzerklärung mit 30 Tagen zusagt) und
+abgelaufene Nachrück-Angebote nie geschlossen: die Warteliste bliebe dann
+beim ersten unbeantworteten Angebot stehen.
- [ ] `registration-closed` — ruft `POST /api/cron/registration-closed` auf
(Vorschlag: alle 6 Stunden), Skript `scripts/trigger-registration-closed.mjs`
- [ ] `newsletter-cleanup` — ruft `POST /api/cron/newsletter-cleanup` auf
(Vorschlag: täglich), Skript `scripts/trigger-newsletter-cleanup.mjs`
+- [ ] `waitlist-offers` — ruft `POST /api/cron/waitlist-offers` auf
+ (Vorschlag: stündlich), Skript `scripts/trigger-waitlist-offers.mjs`;
+ schließt abgelaufene Nachrück-Angebote und erinnert das Kursteam zwei
+ Tage vor Ablauf
-Beide brauchen `Authorization: Bearer $CRON_SECRET` — ohne den Header
+Alle brauchen `Authorization: Bearer $CRON_SECRET` — ohne den Header
antworten die Routen mit 401. Einmal von Hand testen:
```bash
diff --git a/jest.config.ts b/jest.config.ts
index e208c380..432ef505 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -1,9 +1,8 @@
import type { Config } from "jest";
import nextJest from "next/jest.js";
-// next/jest evaluates next.config.js, which imports the t3-env validator.
-// Unit tests are pure and need no real environment — skip validation before
-// the config loads (same escape hatch the Docker build uses).
+// next/jest evaluates next.config.js, which imports the t3-env validator;
+// unit tests need no real environment.
process.env.SKIP_ENV_VALIDATION ??= "1";
const createJestConfig = nextJest({
@@ -14,9 +13,16 @@ const createJestConfig = nextJest({
const config: Config = {
testEnvironment: "node",
testMatch: ["**/__tests__/**/*.test.ts", "**/__tests__/**/*.test.tsx"],
+ // `.claude/worktrees/` enthält vollständige Arbeitskopien; deren Tests und
+ // doppelte Module sollen hier nicht mitlaufen.
+ testPathIgnorePatterns: ["/node_modules/", "/.claude/"],
+ modulePathIgnorePatterns: ["/.claude/"],
moduleNameMapper: {
"^@/(.*)$": "/src/$1",
"^~/(.*)$": "/$1",
+ // `marked` ist nur ESM, und next/jest übersetzt node_modules nicht (nicht
+ // änderbar). Das UMD-Bündel lädt unter CommonJS; die Typen bleiben unberührt.
+ "^marked$": "/node_modules/marked/lib/marked.umd.js",
},
// Pure-logic unit tests only — no DOM, no database.
setupFiles: ["/jest.setup.ts"],
diff --git a/next.config.js b/next.config.js
index 90374465..5035eeeb 100644
--- a/next.config.js
+++ b/next.config.js
@@ -19,6 +19,9 @@ const config = {
},
experimental: {
+ // Sonst 10 MB durch `src/proxy.ts`, zu wenig für Medien-ZIPs; der Rumpf liegt ganz im
+ // Speicher. Die Grenze des vorgelagerten Webservers (mittwald) muss dazu passen.
+ proxyClientMaxBodySize: "100mb",
webpackMemoryOptimizations: true,
optimizePackageImports: [
"recharts",
diff --git a/package.json b/package.json
index e97e05da..633e0731 100644
--- a/package.json
+++ b/package.json
@@ -62,13 +62,13 @@
"leaflet": "^1.9.4",
"lucide-react": "^0.575.0",
"marked": "^18.0.9",
- "next": "16.3.3",
+ "next": "16.3.4",
"nodemailer": "^10.0.1",
"pg": "^8.20.0",
"prisma": "^7.10.0",
"qrcode": "^1.5.4",
- "react": "^19.2.8",
- "react-dom": "^19.2.8",
+ "react": "^19.3.0",
+ "react-dom": "^19.3.0",
"react-image-crop": "^11.1.2",
"recharts": "^3.10.1",
"server-only": "^0.0.1",
@@ -76,23 +76,23 @@
"tsx": "^4.23.12",
"turndown": "^7.2.2",
"vexflow": "^5.0.0",
- "zod": "^4.4.3"
+ "zod": "^4.6.2"
},
"devDependencies": {
- "@eslint/eslintrc": "^3.3.6",
+ "@eslint/eslintrc": "^3.3.7",
"@jest/globals": "^30.4.1",
"@next/eslint-plugin-next": "^16.3.0",
"@tailwindcss/postcss": "^4.3.2",
"@types/dompurify": "^3.2.0",
"@types/leaflet": "^1.9.22",
- "@types/node": "^26.2.0",
+ "@types/node": "^26.5.1",
"@types/nodemailer": "^7.0.10",
"@types/qrcode": "^1.5.6",
- "@types/react": "^19.2.18",
- "@types/react-dom": "^19.2.7",
+ "@types/react": "^19.3.0",
+ "@types/react-dom": "^19.3.0",
"@types/turndown": "^5.0.6",
"baseline-browser-mapping": "^2.11.21",
- "eslint": "^10.8.1",
+ "eslint": "^10.10.0",
"eslint-plugin-react-hooks": "^7.0.1",
"jest": "^30.4.2",
"postcss": "^8.5.26",
@@ -100,7 +100,7 @@
"prettier-plugin-tailwindcss": "^0.8.1",
"tailwindcss": "^4.3.2",
"typescript": "^6.0.3",
- "typescript-eslint": "^8.67.0"
+ "typescript-eslint": "^8.70.0"
},
"ct3aMetadata": {
"initVersion": "7.40.0"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 721d3913..e7895520 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -28,22 +28,22 @@ importers:
version: 7.10.0
'@prisma/client':
specifier: ^7.10.0
- version: 7.10.0(prisma@7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3)
+ version: 7.10.0(prisma@7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(typescript@6.0.3)
'@react-email/components':
specifier: ^1.0.12
- version: 1.0.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ version: 1.0.12(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@t3-oss/env-nextjs':
specifier: ^0.13.11
- version: 0.13.11(typescript@6.0.3)(valibot@1.4.2(typescript@6.0.3))(zod@4.4.3)
+ version: 0.13.11(typescript@6.0.3)(valibot@1.4.2(typescript@6.0.3))(zod@4.6.5)
'@tailwindcss/typography':
specifier: ^0.5.20
version: 0.5.20(tailwindcss@4.3.3)
'@tanstack/react-query':
specifier: ^5.102.8
- version: 5.102.8(react@19.2.8)
+ version: 5.102.8(react@19.3.0)
'@tanstack/react-table':
specifier: ^9.2.4
- version: 9.2.4(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ version: 9.2.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@tiptap/extension-image':
specifier: ^3.31.3
version: 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))
@@ -76,7 +76,7 @@ importers:
version: 3.31.3
'@tiptap/react':
specifier: ^3.31.3
- version: 3.31.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ version: 3.31.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@tiptap/starter-kit':
specifier: ^3.31.3
version: 3.31.3
@@ -85,13 +85,13 @@ importers:
version: 11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3)
'@trpc/react-query':
specifier: ^11.18.0
- version: 11.18.0(@tanstack/react-query@5.102.8(react@19.2.8))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.2.8)(typescript@6.0.3)
+ version: 11.18.0(@tanstack/react-query@5.102.8(react@19.3.0))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.3.0)(typescript@6.0.3)
'@trpc/server':
specifier: ^11.18.0
version: 11.18.0(typescript@6.0.3)
better-auth:
specifier: ^1.7.3
- version: 1.7.3(@prisma/client@7.10.0(prisma@7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3))(mysql2@3.24.3(@types/node@26.2.0))(next@16.3.3(@babel/core@7.29.0)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.20.0)(prisma@7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ version: 1.7.3(@prisma/client@7.10.0(prisma@7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(typescript@6.0.3))(mysql2@3.24.3(@types/node@26.6.1))(next@16.3.4(@babel/core@7.29.0)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.20.0)(prisma@7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
dotenv:
specifier: ^17.4.2
version: 17.4.2
@@ -115,13 +115,13 @@ importers:
version: 1.9.4
lucide-react:
specifier: ^0.575.0
- version: 0.575.0(react@19.2.8)
+ version: 0.575.0(react@19.3.0)
marked:
specifier: ^18.0.9
version: 18.0.9
next:
- specifier: 16.3.3
- version: 16.3.3(@babel/core@7.29.0)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ specifier: 16.3.4
+ version: 16.3.4(@babel/core@7.29.0)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
nodemailer:
specifier: ^10.0.1
version: 10.0.1
@@ -130,22 +130,22 @@ importers:
version: 8.20.0
prisma:
specifier: ^7.10.0
- version: 7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3)
+ version: 7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3)
qrcode:
specifier: ^1.5.4
version: 1.5.4
react:
- specifier: ^19.2.8
- version: 19.2.8
+ specifier: ^19.3.0
+ version: 19.3.0
react-dom:
- specifier: ^19.2.8
- version: 19.2.8(react@19.2.8)
+ specifier: ^19.3.0
+ version: 19.3.0(react@19.3.0)
react-image-crop:
specifier: ^11.1.2
- version: 11.1.2(react@19.2.8)
+ version: 11.1.2(react@19.3.0)
recharts:
specifier: ^3.10.1
- version: 3.10.1(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1)
+ version: 3.10.1(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react-is@19.3.0)(react@19.3.0)(redux@5.0.1)
server-only:
specifier: ^0.0.1
version: 0.0.1
@@ -162,18 +162,18 @@ importers:
specifier: ^5.0.0
version: 5.0.0
zod:
- specifier: ^4.4.3
- version: 4.4.3
+ specifier: ^4.6.2
+ version: 4.6.5
devDependencies:
'@eslint/eslintrc':
- specifier: ^3.3.6
- version: 3.3.6
+ specifier: ^3.3.7
+ version: 3.3.7
'@jest/globals':
specifier: ^30.4.1
version: 30.4.1
'@next/eslint-plugin-next':
specifier: ^16.3.0
- version: 16.3.0(eslint@10.8.1(jiti@2.7.0))
+ version: 16.3.0(eslint@10.10.0(jiti@2.7.0))
'@tailwindcss/postcss':
specifier: ^4.3.2
version: 4.3.3
@@ -184,8 +184,8 @@ importers:
specifier: ^1.9.22
version: 1.9.22
'@types/node':
- specifier: ^26.2.0
- version: 26.2.0
+ specifier: ^26.5.1
+ version: 26.6.1
'@types/nodemailer':
specifier: ^7.0.10
version: 7.0.10
@@ -193,11 +193,11 @@ importers:
specifier: ^1.5.6
version: 1.5.6
'@types/react':
- specifier: ^19.2.18
- version: 19.2.18
+ specifier: ^19.3.0
+ version: 19.3.0
'@types/react-dom':
- specifier: ^19.2.7
- version: 19.2.7(@types/react@19.2.18)
+ specifier: ^19.3.0
+ version: 19.3.0(@types/react@19.3.0)
'@types/turndown':
specifier: ^5.0.6
version: 5.0.6
@@ -205,14 +205,14 @@ importers:
specifier: ^2.11.21
version: 2.11.21
eslint:
- specifier: ^10.8.1
- version: 10.8.1(jiti@2.7.0)
+ specifier: ^10.10.0
+ version: 10.10.0(jiti@2.7.0)
eslint-plugin-react-hooks:
specifier: ^7.0.1
- version: 7.0.1(eslint@10.8.1(jiti@2.7.0))
+ version: 7.0.1(eslint@10.10.0(jiti@2.7.0))
jest:
specifier: ^30.4.2
- version: 30.4.2(@types/node@26.2.0)
+ version: 30.4.2(@types/node@26.6.1)
postcss:
specifier: ^8.5.26
version: 8.5.26
@@ -229,8 +229,8 @@ importers:
specifier: ^6.0.3
version: 6.0.3
typescript-eslint:
- specifier: ^8.67.0
- version: 8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)
+ specifier: ^8.70.0
+ version: 8.70.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)
packages:
@@ -501,6 +501,12 @@ packages:
resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
hasBin: true
+ '@cacheable/memory@2.2.0':
+ resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==}
+
+ '@cacheable/utils@2.5.0':
+ resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==}
+
'@csstools/color-helpers@6.1.1':
resolution: {integrity: sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==}
engines: {node: '>=20.19.0'}
@@ -747,16 +753,16 @@ packages:
resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- '@eslint/eslintrc@3.3.6':
- resolution: {integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==}
+ '@eslint/eslintrc@3.3.7':
+ resolution: {integrity: sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/object-schema@3.0.5':
resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- '@eslint/plugin-kit@0.7.2':
- resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==}
+ '@eslint/plugin-kit@0.7.3':
+ resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@exodus/bytes@1.15.1':
@@ -1075,6 +1081,15 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+ '@keyv/bigmap@1.3.1':
+ resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==}
+ engines: {node: '>= 18'}
+ peerDependencies:
+ keyv: ^5.6.0
+
+ '@keyv/serialize@1.1.1':
+ resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==}
+
'@mixmark-io/domino@2.2.0':
resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==}
@@ -1085,60 +1100,60 @@ packages:
'@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3
'@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3
- '@next/env@16.3.3':
- resolution: {integrity: sha512-U2eYQRwXj+dsqxV79zFqExDdatnNY/ZWc2nsJU1p/OgT7fd3dXwlF6OjYaFQCfMoeTA19PWq+wVmYgimVA+V+g==}
+ '@next/env@16.3.4':
+ resolution: {integrity: sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==}
'@next/eslint-plugin-next@16.3.0':
resolution: {integrity: sha512-OqgJ8PN0d04KcPhDX/PTY5tJUJZxlbrt7O7FBsm4XE0XW2JDrKnDXsc9uo9WUimJGPoo2j+JRGhyXApC//mvbw==}
- '@next/swc-darwin-arm64@16.3.3':
- resolution: {integrity: sha512-8Hiv32QJPwdV6KYJ8meR9SBA061tQqnIKTJDocvOXlEQqib0xMFpzArosuffFUUc0sslbh7QQ8a3Yey1QV8EIw==}
+ '@next/swc-darwin-arm64@16.3.4':
+ resolution: {integrity: sha512-iBr3I5LZNk5/bgl5//iTgD2tcym14MX0Xo7fD//u9dYAEgGzza1y9oywluPtf74YnOswVdH1908aK9xVz7zQTw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@next/swc-darwin-x64@16.3.3':
- resolution: {integrity: sha512-A1lgKgwVchRYmSe467zdwhxT9040dd8lH+o65sL5Jet8fjB4kegw/rDyPIpYVRb6jAqwXFOJpjIXJLxQKLiE3A==}
+ '@next/swc-darwin-x64@16.3.4':
+ resolution: {integrity: sha512-2dpiSyl2Jw/NrBPaU2MAKGSa+2MR82pJIn4Sm5Rjr+gxAeuh0z158Su3Z2O8zn7UNNq+ej4bToed6RcRN/Lydg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@next/swc-linux-arm64-gnu@16.3.3':
- resolution: {integrity: sha512-bf0FIssMFueU2dm7vQEWWxk0c8UjKTdW0yzuh0sQsD8pf1+KCLDdaqhYZNMYGmXwEOiHAUzgBKudovIlcvvBjg==}
+ '@next/swc-linux-arm64-gnu@16.3.4':
+ resolution: {integrity: sha512-+t+U8HZT+fApePCS5h89CSH3datz29MkzyfCn+6fpsZBG/oiEOhINcb9rtkv6sdpToLGFn2e6146NzaKCXkqrA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@next/swc-linux-arm64-musl@16.3.3':
- resolution: {integrity: sha512-W7viwCk9JY/cAkdz/A273rd5bb3RgT/IHwR7Upv90tunjBWNtAAhGhoecHh+teRNRSinuAFmE+l7fwZ4YKkrXg==}
+ '@next/swc-linux-arm64-musl@16.3.4':
+ resolution: {integrity: sha512-mx03GNs1ocQA5JQ4FxDMmIsNkdrZh8cuezKCrId28e5/gIPU/l7Kcy2+vmCCzdjnnmXJy+iOAu+7K0QppO6Urg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@next/swc-linux-x64-gnu@16.3.3':
- resolution: {integrity: sha512-0W46zw1N3ODpI6n0GeivHvvob1pooozgZVqy65k0mh4/7vr+FbY9+WpHzNVXjHipJf/A3FDheBG19H1s5A25rA==}
+ '@next/swc-linux-x64-gnu@16.3.4':
+ resolution: {integrity: sha512-YIhGY6fSMfha52bnVxnzc9zaVBzJg+cqQTOD8tXIBSx4fuv0pVMxQTE0PaS59YhnMOiYiG09IMwxJAf/CFm/Dw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@next/swc-linux-x64-musl@16.3.3':
- resolution: {integrity: sha512-H4mBso8ZTMBPtdT0PN0pBx2ayTvQuTuvS6qT13d77yVFJXAPCxkyIhLTmdMaGTJs0krQYI/qpzdHijCeihXhbg==}
+ '@next/swc-linux-x64-musl@16.3.4':
+ resolution: {integrity: sha512-+eaaX6axpDb0yF1GCpiERe6njplvdC+nks/fKfcHu3XPGRrald8P3/X7yv7QLdjA51knnxwl9pxdIJsg+w1L+Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
- '@next/swc-win32-arm64-msvc@16.3.3':
- resolution: {integrity: sha512-cTMUJpcEGmeywofCUfhR+rSsoE33+rVPnPEYNTNdLNlsOeEg/vktOsKUSTb28vUGqD2jkm4Zaskcwn7OCI6FQg==}
+ '@next/swc-win32-arm64-msvc@16.3.4':
+ resolution: {integrity: sha512-0jcXW7Xs/uzICrmgV3MhDYDeRy++1CqnpDIerlPIqYO4bhzB4WNbX/aRnQclustsAyTkFKB0z6rbcjmNg5tR8A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@next/swc-win32-x64-msvc@16.3.3':
- resolution: {integrity: sha512-2VR4cTBzHXaBjnGsuH6GyJjENzQOmHeAh11uY1iUhjm3j5dEUrVJuUj+VL78jaGi/Dik8xS76zEj18BsFhlVZQ==}
+ '@next/swc-win32-x64-msvc@16.3.4':
+ resolution: {integrity: sha512-vvBzwu1pYQCp92maZCFCIw/XgOTMR5tur9GjakwIo2cmwRTMKajRZZDS9+e4KsUZWKu1E007WUeAFXRRjZeuzw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -1998,6 +2013,9 @@ packages:
'@types/node@26.2.0':
resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==}
+ '@types/node@26.6.1':
+ resolution: {integrity: sha512-VqGJBMCtdhqkBUCcBLvywI0NJ+KLuVzgNnlBUNFOQjqVxzo2lxLUNg1DSey8+u2u6ktswSAxg+s68QLzWHNOuA==}
+
'@types/nodemailer@7.0.10':
resolution: {integrity: sha512-tP+9WggTFN22Zxh0XFyst7239H0qwiRCogsk7v9aQS79sYAJY+WEbTHbNYcxUMaalHKmsNpxmoTe35hBEMMd6g==}
@@ -2013,14 +2031,17 @@ packages:
'@types/raf@3.4.3':
resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
- '@types/react-dom@19.2.7':
- resolution: {integrity: sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==}
+ '@types/react-dom@19.3.0':
+ resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==}
peerDependencies:
- '@types/react': ^19.2.0
+ '@types/react': ^19.3.0
'@types/react@19.2.18':
resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==}
+ '@types/react@19.3.0':
+ resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==}
+
'@types/stack-utils@2.0.3':
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
@@ -2039,63 +2060,63 @@ packages:
'@types/yargs@17.0.35':
resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==}
- '@typescript-eslint/eslint-plugin@8.67.0':
- resolution: {integrity: sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==}
+ '@typescript-eslint/eslint-plugin@8.70.0':
+ resolution: {integrity: sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- '@typescript-eslint/parser': ^8.67.0
+ '@typescript-eslint/parser': ^8.70.0
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/parser@8.67.0':
- resolution: {integrity: sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==}
+ '@typescript-eslint/parser@8.70.0':
+ resolution: {integrity: sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/project-service@8.67.0':
- resolution: {integrity: sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==}
+ '@typescript-eslint/project-service@8.70.0':
+ resolution: {integrity: sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/scope-manager@8.67.0':
- resolution: {integrity: sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==}
+ '@typescript-eslint/scope-manager@8.70.0':
+ resolution: {integrity: sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/tsconfig-utils@8.67.0':
- resolution: {integrity: sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==}
+ '@typescript-eslint/tsconfig-utils@8.70.0':
+ resolution: {integrity: sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/type-utils@8.67.0':
- resolution: {integrity: sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==}
+ '@typescript-eslint/type-utils@8.70.0':
+ resolution: {integrity: sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/types@8.67.0':
- resolution: {integrity: sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==}
+ '@typescript-eslint/types@8.70.0':
+ resolution: {integrity: sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/typescript-estree@8.67.0':
- resolution: {integrity: sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==}
+ '@typescript-eslint/typescript-estree@8.70.0':
+ resolution: {integrity: sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/utils@8.67.0':
- resolution: {integrity: sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==}
+ '@typescript-eslint/utils@8.70.0':
+ resolution: {integrity: sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/visitor-keys@8.67.0':
- resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==}
+ '@typescript-eslint/visitor-keys@8.70.0':
+ resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@ungap/structured-clone@1.3.3':
@@ -2505,6 +2526,9 @@ packages:
magicast:
optional: true
+ cacheable@2.5.0:
+ resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==}
+
callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
@@ -2869,8 +2893,8 @@ packages:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- eslint@10.8.1:
- resolution: {integrity: sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==}
+ eslint@10.10.0:
+ resolution: {integrity: sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
hasBin: true
peerDependencies:
@@ -2985,9 +3009,8 @@ packages:
fflate@0.8.3:
resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==}
- file-entry-cache@8.0.0:
- resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
- engines: {node: '>=16.0.0'}
+ file-entry-cache@11.1.5:
+ resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
@@ -3005,9 +3028,8 @@ packages:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'}
- flat-cache@4.0.1:
- resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
- engines: {node: '>=16'}
+ flat-cache@6.1.23:
+ resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==}
flatted@3.4.4:
resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==}
@@ -3092,12 +3114,22 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
+ hashery@1.5.1:
+ resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==}
+ engines: {node: '>=20'}
+
hermes-estree@0.25.1:
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
hermes-parser@0.25.1:
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
+ hookified@1.15.1:
+ resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==}
+
+ hookified@2.2.0:
+ resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==}
+
html-encoding-sniffer@6.0.0:
resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
@@ -3134,8 +3166,8 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
- ignore@7.0.6:
- resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==}
+ ignore@7.0.9:
+ resolution: {integrity: sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==}
engines: {node: '>= 4'}
immediate@3.0.6:
@@ -3405,9 +3437,6 @@ packages:
engines: {node: '>=6'}
hasBin: true
- json-buffer@3.0.1:
- resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
-
json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
@@ -3431,8 +3460,8 @@ packages:
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
- keyv@4.5.4:
- resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+ keyv@5.6.0:
+ resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==}
kysely@0.29.5:
resolution: {integrity: sha512-ooa+eSbBNPTo3MycPEuW5jdrxQdQwdtB3LC3h43FiXQbIry5tR0C5lDG7eealK0E4D7XjrnOP5DIUg/LyjRMYQ==}
@@ -3697,8 +3726,8 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
- nanoid@3.3.18:
- resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
+ nanoid@3.3.19:
+ resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
@@ -3714,8 +3743,8 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
- next@16.3.3:
- resolution: {integrity: sha512-tuRTx1nQ/yVw83cwJBo9F+njGUgMn3UHQycreWHB8XsStvvAh1AthbI8/4IpKnFaF58F+iSiHejYOlMQ/eq83g==}
+ next@16.3.4:
+ resolution: {integrity: sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -3889,8 +3918,8 @@ packages:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
- picomatch@4.0.5:
- resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
+ picomatch@4.0.7:
+ resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
engines: {node: '>=12'}
pirates@4.0.7:
@@ -4084,6 +4113,10 @@ packages:
pure-rand@7.0.1:
resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==}
+ qified@0.10.1:
+ resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==}
+ engines: {node: '>=20'}
+
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
@@ -4098,10 +4131,10 @@ packages:
rc9@3.1.0:
resolution: {integrity: sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==}
- react-dom@19.2.8:
- resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
+ react-dom@19.3.0:
+ resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==}
peerDependencies:
- react: ^19.2.8
+ react: ^19.3.0
react-image-crop@11.1.2:
resolution: {integrity: sha512-+0Pc2fxpwKL4u4oLmdKBw8XSwUceFbXbKEHvFOlsl/MGB1OVNic4uBlAPmEHGXYgoJIq+b63xHbc/aJMG0AVkA==}
@@ -4111,8 +4144,8 @@ packages:
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
- react-is@19.2.8:
- resolution: {integrity: sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==}
+ react-is@19.3.0:
+ resolution: {integrity: sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA==}
react-redux@9.2.0:
resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
@@ -4126,8 +4159,8 @@ packages:
redux:
optional: true
- react@19.2.8:
- resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
+ react@19.3.0:
+ resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==}
engines: {node: '>=0.10.0'}
readable-stream@2.3.8:
@@ -4246,8 +4279,8 @@ packages:
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
engines: {node: '>=v12.22.7'}
- scheduler@0.27.0:
- resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+ scheduler@0.28.0:
+ resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==}
selderee@0.11.0:
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
@@ -4491,8 +4524,8 @@ packages:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
- typescript-eslint@8.67.0:
- resolution: {integrity: sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==}
+ typescript-eslint@8.70.0:
+ resolution: {integrity: sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@@ -4506,6 +4539,9 @@ packages:
undici-types@8.3.0:
resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
+ undici-types@8.9.0:
+ resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
+
undici@8.10.0:
resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==}
engines: {node: '>=22.19.0'}
@@ -4670,12 +4706,12 @@ packages:
peerDependencies:
zod: ^3.25.0 || ^4.0.0
- zod@4.4.3:
- resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
-
zod@4.5.4:
resolution: {integrity: sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==}
+ zod@4.6.5:
+ resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==}
+
snapshots:
'@alloc/quick-lru@5.2.0': {}
@@ -4886,7 +4922,7 @@ snapshots:
'@bcoe/v8-coverage@0.2.3': {}
- '@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)':
+ '@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)':
dependencies:
'@better-auth/utils': 0.4.2
'@better-fetch/fetch': 1.3.1
@@ -4898,39 +4934,39 @@ snapshots:
nanostores: 1.5.3
zod: 4.5.4
- '@better-auth/drizzle-adapter@1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)':
+ '@better-auth/drizzle-adapter@1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)':
dependencies:
- '@better-auth/core': 1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)
+ '@better-auth/core': 1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)
'@better-auth/utils': 0.4.2
- '@better-auth/kysely-adapter@1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(kysely@0.29.5)':
+ '@better-auth/kysely-adapter@1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(kysely@0.29.5)':
dependencies:
- '@better-auth/core': 1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)
+ '@better-auth/core': 1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)
'@better-auth/utils': 0.4.2
optionalDependencies:
kysely: 0.29.5
- '@better-auth/memory-adapter@1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)':
+ '@better-auth/memory-adapter@1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)':
dependencies:
- '@better-auth/core': 1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)
+ '@better-auth/core': 1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)
'@better-auth/utils': 0.4.2
- '@better-auth/mongo-adapter@1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)':
+ '@better-auth/mongo-adapter@1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)':
dependencies:
- '@better-auth/core': 1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)
+ '@better-auth/core': 1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)
'@better-auth/utils': 0.4.2
- '@better-auth/prisma-adapter@1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@prisma/client@7.10.0(prisma@7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3))(prisma@7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))':
+ '@better-auth/prisma-adapter@1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@prisma/client@7.10.0(prisma@7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(typescript@6.0.3))(prisma@7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))':
dependencies:
- '@better-auth/core': 1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)
+ '@better-auth/core': 1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)
'@better-auth/utils': 0.4.2
optionalDependencies:
- '@prisma/client': 7.10.0(prisma@7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3)
- prisma: 7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3)
+ '@prisma/client': 7.10.0(prisma@7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(typescript@6.0.3)
+ prisma: 7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3)
- '@better-auth/telemetry@1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)':
+ '@better-auth/telemetry@1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)':
dependencies:
- '@better-auth/core': 1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)
+ '@better-auth/core': 1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)
'@better-auth/utils': 0.4.2
'@better-fetch/fetch': 1.3.1
@@ -4948,6 +4984,18 @@ snapshots:
dependencies:
css-tree: 3.2.1
+ '@cacheable/memory@2.2.0':
+ dependencies:
+ '@cacheable/utils': 2.5.0
+ '@keyv/bigmap': 1.3.1(keyv@5.6.0)
+ hookified: 1.15.1
+ keyv: 5.6.0
+
+ '@cacheable/utils@2.5.0':
+ dependencies:
+ hashery: 1.5.1
+ keyv: 5.6.0
+
'@csstools/color-helpers@6.1.1': {}
'@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
@@ -5081,14 +5129,14 @@ snapshots:
'@esbuild/win32-x64@0.28.2':
optional: true
- '@eslint-community/eslint-utils@4.10.1(eslint@10.8.1(jiti@2.7.0))':
+ '@eslint-community/eslint-utils@4.10.1(eslint@10.10.0(jiti@2.7.0))':
dependencies:
- eslint: 10.8.1(jiti@2.7.0)
+ eslint: 10.10.0(jiti@2.7.0)
eslint-visitor-keys: 3.4.3
- '@eslint-community/eslint-utils@4.9.1(eslint@10.8.1(jiti@2.7.0))':
+ '@eslint-community/eslint-utils@4.9.1(eslint@10.10.0(jiti@2.7.0))':
dependencies:
- eslint: 10.8.1(jiti@2.7.0)
+ eslint: 10.10.0(jiti@2.7.0)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
@@ -5109,7 +5157,7 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
- '@eslint/eslintrc@3.3.6':
+ '@eslint/eslintrc@3.3.7':
dependencies:
ajv: 6.15.0
debug: 4.4.3
@@ -5125,7 +5173,7 @@ snapshots:
'@eslint/object-schema@3.0.5': {}
- '@eslint/plugin-kit@0.7.2':
+ '@eslint/plugin-kit@0.7.3':
dependencies:
'@eslint/core': 1.2.1
levn: 0.4.1
@@ -5506,6 +5554,14 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@keyv/bigmap@1.3.1(keyv@5.6.0)':
+ dependencies:
+ hashery: 1.5.1
+ hookified: 1.15.1
+ keyv: 5.6.0
+
+ '@keyv/serialize@1.1.1': {}
+
'@mixmark-io/domino@2.2.0': {}
'@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
@@ -5515,37 +5571,37 @@ snapshots:
'@tybys/wasm-util': 0.10.3
optional: true
- '@next/env@16.3.3': {}
+ '@next/env@16.3.4': {}
- '@next/eslint-plugin-next@16.3.0(eslint@10.8.1(jiti@2.7.0))':
+ '@next/eslint-plugin-next@16.3.0(eslint@10.10.0(jiti@2.7.0))':
dependencies:
- '@eslint-community/eslint-utils': 4.9.1(eslint@10.8.1(jiti@2.7.0))
+ '@eslint-community/eslint-utils': 4.9.1(eslint@10.10.0(jiti@2.7.0))
fast-glob: 3.3.1
transitivePeerDependencies:
- eslint
- '@next/swc-darwin-arm64@16.3.3':
+ '@next/swc-darwin-arm64@16.3.4':
optional: true
- '@next/swc-darwin-x64@16.3.3':
+ '@next/swc-darwin-x64@16.3.4':
optional: true
- '@next/swc-linux-arm64-gnu@16.3.3':
+ '@next/swc-linux-arm64-gnu@16.3.4':
optional: true
- '@next/swc-linux-arm64-musl@16.3.3':
+ '@next/swc-linux-arm64-musl@16.3.4':
optional: true
- '@next/swc-linux-x64-gnu@16.3.3':
+ '@next/swc-linux-x64-gnu@16.3.4':
optional: true
- '@next/swc-linux-x64-musl@16.3.3':
+ '@next/swc-linux-x64-musl@16.3.4':
optional: true
- '@next/swc-win32-arm64-msvc@16.3.3':
+ '@next/swc-win32-arm64-msvc@16.3.4':
optional: true
- '@next/swc-win32-x64-msvc@16.3.3':
+ '@next/swc-win32-x64-msvc@16.3.4':
optional: true
'@noble/ciphers@2.4.0': {}
@@ -5582,11 +5638,11 @@ snapshots:
'@prisma/client-runtime-utils@7.10.0': {}
- '@prisma/client@7.10.0(prisma@7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3)':
+ '@prisma/client@7.10.0(prisma@7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(typescript@6.0.3)':
dependencies:
'@prisma/client-runtime-utils': 7.10.0
optionalDependencies:
- prisma: 7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3)
+ prisma: 7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3)
typescript: 6.0.3
'@prisma/config@7.10.0':
@@ -5658,206 +5714,206 @@ snapshots:
env-paths: 3.0.0
proper-lockfile: 4.1.2
- '@prisma/studio-core@0.33.0(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ '@prisma/studio-core@0.33.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
- '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
- '@types/react': 19.2.18
+ '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@types/react': 19.3.0
'@visx/curve': 4.0.1-alpha.0
'@visx/event': 4.0.1-alpha.0
- '@visx/grid': 4.0.1-alpha.0(react@19.2.8)
- '@visx/group': 4.0.1-alpha.0(react@19.2.8)
- '@visx/responsive': 4.0.1-alpha.0(react@19.2.8)
+ '@visx/grid': 4.0.1-alpha.0(react@19.3.0)
+ '@visx/group': 4.0.1-alpha.0(react@19.3.0)
+ '@visx/responsive': 4.0.1-alpha.0(react@19.3.0)
'@visx/scale': 4.0.1-alpha.0
- '@visx/shape': 4.0.1-alpha.0(react@19.2.8)
+ '@visx/shape': 4.0.1-alpha.0(react@19.3.0)
d3-array: 3.2.4
d3-shape: 3.2.0
elkjs: 0.11.1
- react: 19.2.8
- react-dom: 19.2.8(react@19.2.8)
+ react: 19.3.0
+ react-dom: 19.3.0(react@19.3.0)
transitivePeerDependencies:
- '@types/react-dom'
'@radix-ui/primitive@1.1.3': {}
- '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.18)(react@19.2.8)':
+ '@radix-ui/react-compose-refs@1.1.2(@types/react@19.3.0)(react@19.3.0)':
dependencies:
- react: 19.2.8
+ react: 19.3.0
optionalDependencies:
- '@types/react': 19.2.18
+ '@types/react': 19.3.0
- '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
- '@radix-ui/react-slot': 1.2.3(@types/react@19.2.18)(react@19.2.8)
- react: 19.2.8
- react-dom: 19.2.8(react@19.2.8)
+ '@radix-ui/react-slot': 1.2.3(@types/react@19.3.0)(react@19.3.0)
+ react: 19.3.0
+ react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
- '@types/react': 19.2.18
- '@types/react-dom': 19.2.7(@types/react@19.2.18)
+ '@types/react': 19.3.0
+ '@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-slot@1.2.3(@types/react@19.2.18)(react@19.2.8)':
+ '@radix-ui/react-slot@1.2.3(@types/react@19.3.0)(react@19.3.0)':
dependencies:
- '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.18)(react@19.2.8)
- react: 19.2.8
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.3.0)(react@19.3.0)
+ react: 19.3.0
optionalDependencies:
- '@types/react': 19.2.18
+ '@types/react': 19.3.0
- '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
- '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
- '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.18)(react@19.2.8)
- react: 19.2.8
- react-dom: 19.2.8(react@19.2.8)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.3.0)(react@19.3.0)
+ react: 19.3.0
+ react-dom: 19.3.0(react@19.3.0)
optionalDependencies:
- '@types/react': 19.2.18
- '@types/react-dom': 19.2.7(@types/react@19.2.18)
+ '@types/react': 19.3.0
+ '@types/react-dom': 19.3.0(@types/react@19.3.0)
- '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.18)(react@19.2.8)':
+ '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.3.0)(react@19.3.0)':
dependencies:
- '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.18)(react@19.2.8)
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.18)(react@19.2.8)
- react: 19.2.8
+ '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.3.0)(react@19.3.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.3.0)(react@19.3.0)
+ react: 19.3.0
optionalDependencies:
- '@types/react': 19.2.18
+ '@types/react': 19.3.0
- '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.18)(react@19.2.8)':
+ '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.3.0)(react@19.3.0)':
dependencies:
- '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.18)(react@19.2.8)
- react: 19.2.8
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.3.0)(react@19.3.0)
+ react: 19.3.0
optionalDependencies:
- '@types/react': 19.2.18
+ '@types/react': 19.3.0
- '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.18)(react@19.2.8)':
+ '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.3.0)(react@19.3.0)':
dependencies:
- react: 19.2.8
+ react: 19.3.0
optionalDependencies:
- '@types/react': 19.2.18
+ '@types/react': 19.3.0
- '@react-email/body@0.3.0(react@19.2.8)':
+ '@react-email/body@0.3.0(react@19.3.0)':
dependencies:
- react: 19.2.8
+ react: 19.3.0
- '@react-email/button@0.2.1(react@19.2.8)':
+ '@react-email/button@0.2.1(react@19.3.0)':
dependencies:
- react: 19.2.8
+ react: 19.3.0
- '@react-email/code-block@0.2.1(react@19.2.8)':
+ '@react-email/code-block@0.2.1(react@19.3.0)':
dependencies:
prismjs: 1.30.0
- react: 19.2.8
-
- '@react-email/code-inline@0.0.6(react@19.2.8)':
- dependencies:
- react: 19.2.8
-
- '@react-email/column@0.0.14(react@19.2.8)':
- dependencies:
- react: 19.2.8
-
- '@react-email/components@1.0.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
- dependencies:
- '@react-email/body': 0.3.0(react@19.2.8)
- '@react-email/button': 0.2.1(react@19.2.8)
- '@react-email/code-block': 0.2.1(react@19.2.8)
- '@react-email/code-inline': 0.0.6(react@19.2.8)
- '@react-email/column': 0.0.14(react@19.2.8)
- '@react-email/container': 0.0.16(react@19.2.8)
- '@react-email/font': 0.0.10(react@19.2.8)
- '@react-email/head': 0.0.13(react@19.2.8)
- '@react-email/heading': 0.0.16(react@19.2.8)
- '@react-email/hr': 0.0.12(react@19.2.8)
- '@react-email/html': 0.0.12(react@19.2.8)
- '@react-email/img': 0.0.12(react@19.2.8)
- '@react-email/link': 0.0.13(react@19.2.8)
- '@react-email/markdown': 0.0.18(react@19.2.8)
- '@react-email/preview': 0.0.14(react@19.2.8)
- '@react-email/render': 2.0.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
- '@react-email/row': 0.0.13(react@19.2.8)
- '@react-email/section': 0.0.17(react@19.2.8)
- '@react-email/tailwind': 2.0.7(@react-email/body@0.3.0(react@19.2.8))(@react-email/button@0.2.1(react@19.2.8))(@react-email/code-block@0.2.1(react@19.2.8))(@react-email/code-inline@0.0.6(react@19.2.8))(@react-email/container@0.0.16(react@19.2.8))(@react-email/heading@0.0.16(react@19.2.8))(@react-email/hr@0.0.12(react@19.2.8))(@react-email/img@0.0.12(react@19.2.8))(@react-email/link@0.0.13(react@19.2.8))(@react-email/preview@0.0.14(react@19.2.8))(@react-email/text@0.1.6(react@19.2.8))(react@19.2.8)
- '@react-email/text': 0.1.6(react@19.2.8)
- react: 19.2.8
+ react: 19.3.0
+
+ '@react-email/code-inline@0.0.6(react@19.3.0)':
+ dependencies:
+ react: 19.3.0
+
+ '@react-email/column@0.0.14(react@19.3.0)':
+ dependencies:
+ react: 19.3.0
+
+ '@react-email/components@1.0.12(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
+ dependencies:
+ '@react-email/body': 0.3.0(react@19.3.0)
+ '@react-email/button': 0.2.1(react@19.3.0)
+ '@react-email/code-block': 0.2.1(react@19.3.0)
+ '@react-email/code-inline': 0.0.6(react@19.3.0)
+ '@react-email/column': 0.0.14(react@19.3.0)
+ '@react-email/container': 0.0.16(react@19.3.0)
+ '@react-email/font': 0.0.10(react@19.3.0)
+ '@react-email/head': 0.0.13(react@19.3.0)
+ '@react-email/heading': 0.0.16(react@19.3.0)
+ '@react-email/hr': 0.0.12(react@19.3.0)
+ '@react-email/html': 0.0.12(react@19.3.0)
+ '@react-email/img': 0.0.12(react@19.3.0)
+ '@react-email/link': 0.0.13(react@19.3.0)
+ '@react-email/markdown': 0.0.18(react@19.3.0)
+ '@react-email/preview': 0.0.14(react@19.3.0)
+ '@react-email/render': 2.0.6(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ '@react-email/row': 0.0.13(react@19.3.0)
+ '@react-email/section': 0.0.17(react@19.3.0)
+ '@react-email/tailwind': 2.0.7(@react-email/body@0.3.0(react@19.3.0))(@react-email/button@0.2.1(react@19.3.0))(@react-email/code-block@0.2.1(react@19.3.0))(@react-email/code-inline@0.0.6(react@19.3.0))(@react-email/container@0.0.16(react@19.3.0))(@react-email/heading@0.0.16(react@19.3.0))(@react-email/hr@0.0.12(react@19.3.0))(@react-email/img@0.0.12(react@19.3.0))(@react-email/link@0.0.13(react@19.3.0))(@react-email/preview@0.0.14(react@19.3.0))(@react-email/text@0.1.6(react@19.3.0))(react@19.3.0)
+ '@react-email/text': 0.1.6(react@19.3.0)
+ react: 19.3.0
transitivePeerDependencies:
- react-dom
- '@react-email/container@0.0.16(react@19.2.8)':
+ '@react-email/container@0.0.16(react@19.3.0)':
dependencies:
- react: 19.2.8
+ react: 19.3.0
- '@react-email/font@0.0.10(react@19.2.8)':
+ '@react-email/font@0.0.10(react@19.3.0)':
dependencies:
- react: 19.2.8
+ react: 19.3.0
- '@react-email/head@0.0.13(react@19.2.8)':
+ '@react-email/head@0.0.13(react@19.3.0)':
dependencies:
- react: 19.2.8
+ react: 19.3.0
- '@react-email/heading@0.0.16(react@19.2.8)':
+ '@react-email/heading@0.0.16(react@19.3.0)':
dependencies:
- react: 19.2.8
+ react: 19.3.0
- '@react-email/hr@0.0.12(react@19.2.8)':
+ '@react-email/hr@0.0.12(react@19.3.0)':
dependencies:
- react: 19.2.8
+ react: 19.3.0
- '@react-email/html@0.0.12(react@19.2.8)':
+ '@react-email/html@0.0.12(react@19.3.0)':
dependencies:
- react: 19.2.8
+ react: 19.3.0
- '@react-email/img@0.0.12(react@19.2.8)':
+ '@react-email/img@0.0.12(react@19.3.0)':
dependencies:
- react: 19.2.8
+ react: 19.3.0
- '@react-email/link@0.0.13(react@19.2.8)':
+ '@react-email/link@0.0.13(react@19.3.0)':
dependencies:
- react: 19.2.8
+ react: 19.3.0
- '@react-email/markdown@0.0.18(react@19.2.8)':
+ '@react-email/markdown@0.0.18(react@19.3.0)':
dependencies:
marked: 15.0.12
- react: 19.2.8
+ react: 19.3.0
- '@react-email/preview@0.0.14(react@19.2.8)':
+ '@react-email/preview@0.0.14(react@19.3.0)':
dependencies:
- react: 19.2.8
+ react: 19.3.0
- '@react-email/render@2.0.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ '@react-email/render@2.0.6(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
html-to-text: 9.0.5
prettier: 3.9.6
- react: 19.2.8
- react-dom: 19.2.8(react@19.2.8)
+ react: 19.3.0
+ react-dom: 19.3.0(react@19.3.0)
- '@react-email/row@0.0.13(react@19.2.8)':
+ '@react-email/row@0.0.13(react@19.3.0)':
dependencies:
- react: 19.2.8
+ react: 19.3.0
- '@react-email/section@0.0.17(react@19.2.8)':
+ '@react-email/section@0.0.17(react@19.3.0)':
dependencies:
- react: 19.2.8
+ react: 19.3.0
- '@react-email/tailwind@2.0.7(@react-email/body@0.3.0(react@19.2.8))(@react-email/button@0.2.1(react@19.2.8))(@react-email/code-block@0.2.1(react@19.2.8))(@react-email/code-inline@0.0.6(react@19.2.8))(@react-email/container@0.0.16(react@19.2.8))(@react-email/heading@0.0.16(react@19.2.8))(@react-email/hr@0.0.12(react@19.2.8))(@react-email/img@0.0.12(react@19.2.8))(@react-email/link@0.0.13(react@19.2.8))(@react-email/preview@0.0.14(react@19.2.8))(@react-email/text@0.1.6(react@19.2.8))(react@19.2.8)':
+ '@react-email/tailwind@2.0.7(@react-email/body@0.3.0(react@19.3.0))(@react-email/button@0.2.1(react@19.3.0))(@react-email/code-block@0.2.1(react@19.3.0))(@react-email/code-inline@0.0.6(react@19.3.0))(@react-email/container@0.0.16(react@19.3.0))(@react-email/heading@0.0.16(react@19.3.0))(@react-email/hr@0.0.12(react@19.3.0))(@react-email/img@0.0.12(react@19.3.0))(@react-email/link@0.0.13(react@19.3.0))(@react-email/preview@0.0.14(react@19.3.0))(@react-email/text@0.1.6(react@19.3.0))(react@19.3.0)':
dependencies:
- '@react-email/text': 0.1.6(react@19.2.8)
- react: 19.2.8
+ '@react-email/text': 0.1.6(react@19.3.0)
+ react: 19.3.0
tailwindcss: 4.2.0
optionalDependencies:
- '@react-email/body': 0.3.0(react@19.2.8)
- '@react-email/button': 0.2.1(react@19.2.8)
- '@react-email/code-block': 0.2.1(react@19.2.8)
- '@react-email/code-inline': 0.0.6(react@19.2.8)
- '@react-email/container': 0.0.16(react@19.2.8)
- '@react-email/heading': 0.0.16(react@19.2.8)
- '@react-email/hr': 0.0.12(react@19.2.8)
- '@react-email/img': 0.0.12(react@19.2.8)
- '@react-email/link': 0.0.13(react@19.2.8)
- '@react-email/preview': 0.0.14(react@19.2.8)
+ '@react-email/body': 0.3.0(react@19.3.0)
+ '@react-email/button': 0.2.1(react@19.3.0)
+ '@react-email/code-block': 0.2.1(react@19.3.0)
+ '@react-email/code-inline': 0.0.6(react@19.3.0)
+ '@react-email/container': 0.0.16(react@19.3.0)
+ '@react-email/heading': 0.0.16(react@19.3.0)
+ '@react-email/hr': 0.0.12(react@19.3.0)
+ '@react-email/img': 0.0.12(react@19.3.0)
+ '@react-email/link': 0.0.13(react@19.3.0)
+ '@react-email/preview': 0.0.14(react@19.3.0)
- '@react-email/text@0.1.6(react@19.2.8)':
+ '@react-email/text@0.1.6(react@19.3.0)':
dependencies:
- react: 19.2.8
+ react: 19.3.0
- '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1))(react@19.2.8)':
+ '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.3.0)(react@19.3.0)(redux@5.0.1))(react@19.3.0)':
dependencies:
'@standard-schema/spec': 1.1.0
'@standard-schema/utils': 0.3.0
@@ -5866,8 +5922,8 @@ snapshots:
redux-thunk: 3.1.0(redux@5.0.1)
reselect: 5.1.1
optionalDependencies:
- react: 19.2.8
- react-redux: 9.2.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1)
+ react: 19.3.0
+ react-redux: 9.2.0(@types/react@19.3.0)(react@19.3.0)(redux@5.0.1)
'@selderee/plugin-htmlparser2@0.11.0':
dependencies:
@@ -5892,19 +5948,19 @@ snapshots:
dependencies:
tslib: 2.8.1
- '@t3-oss/env-core@0.13.11(typescript@6.0.3)(valibot@1.4.2(typescript@6.0.3))(zod@4.4.3)':
+ '@t3-oss/env-core@0.13.11(typescript@6.0.3)(valibot@1.4.2(typescript@6.0.3))(zod@4.6.5)':
optionalDependencies:
typescript: 6.0.3
valibot: 1.4.2(typescript@6.0.3)
- zod: 4.4.3
+ zod: 4.6.5
- '@t3-oss/env-nextjs@0.13.11(typescript@6.0.3)(valibot@1.4.2(typescript@6.0.3))(zod@4.4.3)':
+ '@t3-oss/env-nextjs@0.13.11(typescript@6.0.3)(valibot@1.4.2(typescript@6.0.3))(zod@4.6.5)':
dependencies:
- '@t3-oss/env-core': 0.13.11(typescript@6.0.3)(valibot@1.4.2(typescript@6.0.3))(zod@4.4.3)
+ '@t3-oss/env-core': 0.13.11(typescript@6.0.3)(valibot@1.4.2(typescript@6.0.3))(zod@4.6.5)
optionalDependencies:
typescript: 6.0.3
valibot: 1.4.2(typescript@6.0.3)
- zod: 4.4.3
+ zod: 4.6.5
'@tailwindcss/node@4.3.3':
dependencies:
@@ -5982,23 +6038,23 @@ snapshots:
'@tanstack/query-core@5.102.8': {}
- '@tanstack/react-query@5.102.8(react@19.2.8)':
+ '@tanstack/react-query@5.102.8(react@19.3.0)':
dependencies:
'@tanstack/query-core': 5.102.8
- react: 19.2.8
+ react: 19.3.0
- '@tanstack/react-store@0.11.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ '@tanstack/react-store@0.11.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@tanstack/store': 0.11.1
- react: 19.2.8
- react-dom: 19.2.8(react@19.2.8)
- use-sync-external-store: 1.6.0(react@19.2.8)
+ react: 19.3.0
+ react-dom: 19.3.0(react@19.3.0)
+ use-sync-external-store: 1.6.0(react@19.3.0)
- '@tanstack/react-table@9.2.4(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ '@tanstack/react-table@9.2.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
- '@tanstack/react-store': 0.11.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@tanstack/react-store': 0.11.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
'@tanstack/table-core': 9.2.4
- react: 19.2.8
+ react: 19.3.0
transitivePeerDependencies:
- react-dom
@@ -6166,17 +6222,17 @@ snapshots:
prosemirror-transform: 1.12.1
prosemirror-view: 1.42.3
- '@tiptap/react@3.31.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
+ '@tiptap/react@3.31.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
dependencies:
'@tiptap/core': 3.31.3(@tiptap/pm@3.31.3)
'@tiptap/pm': 3.31.3
- '@types/react': 19.2.18
- '@types/react-dom': 19.2.7(@types/react@19.2.18)
+ '@types/react': 19.3.0
+ '@types/react-dom': 19.3.0(@types/react@19.3.0)
'@types/use-sync-external-store': 0.0.6
fast-equals: 5.4.2
- react: 19.2.8
- react-dom: 19.2.8(react@19.2.8)
- use-sync-external-store: 1.6.0(react@19.2.8)
+ react: 19.3.0
+ react-dom: 19.3.0(react@19.3.0)
+ use-sync-external-store: 1.6.0(react@19.3.0)
optionalDependencies:
'@tiptap/extension-bubble-menu': 3.31.3(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)
'@tiptap/extension-floating-menu': 3.31.3(@floating-ui/dom@1.8.0)(@tiptap/core@3.31.3(@tiptap/pm@3.31.3))(@tiptap/pm@3.31.3)
@@ -6215,12 +6271,12 @@ snapshots:
'@trpc/server': 11.18.0(typescript@6.0.3)
typescript: 6.0.3
- '@trpc/react-query@11.18.0(@tanstack/react-query@5.102.8(react@19.2.8))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.2.8)(typescript@6.0.3)':
+ '@trpc/react-query@11.18.0(@tanstack/react-query@5.102.8(react@19.3.0))(@trpc/client@11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3))(@trpc/server@11.18.0(typescript@6.0.3))(react@19.3.0)(typescript@6.0.3)':
dependencies:
- '@tanstack/react-query': 5.102.8(react@19.2.8)
+ '@tanstack/react-query': 5.102.8(react@19.3.0)
'@trpc/client': 11.18.0(@trpc/server@11.18.0(typescript@6.0.3))(typescript@6.0.3)
'@trpc/server': 11.18.0(typescript@6.0.3)
- react: 19.2.8
+ react: 19.3.0
typescript: 6.0.3
'@trpc/server@11.18.0(typescript@6.0.3)':
@@ -6339,6 +6395,10 @@ snapshots:
dependencies:
undici-types: 8.3.0
+ '@types/node@26.6.1':
+ dependencies:
+ undici-types: 8.9.0
+
'@types/nodemailer@7.0.10':
dependencies:
'@types/node': 26.2.0
@@ -6358,14 +6418,18 @@ snapshots:
'@types/raf@3.4.3':
optional: true
- '@types/react-dom@19.2.7(@types/react@19.2.18)':
+ '@types/react-dom@19.3.0(@types/react@19.3.0)':
dependencies:
- '@types/react': 19.2.18
+ '@types/react': 19.3.0
'@types/react@19.2.18':
dependencies:
csstype: 3.2.3
+ '@types/react@19.3.0':
+ dependencies:
+ csstype: 3.2.3
+
'@types/stack-utils@2.0.3': {}
'@types/trusted-types@2.0.7':
@@ -6381,72 +6445,72 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
- '@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)':
+ '@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
- '@typescript-eslint/parser': 8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)
- '@typescript-eslint/scope-manager': 8.67.0
- '@typescript-eslint/type-utils': 8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)
- '@typescript-eslint/utils': 8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)
- '@typescript-eslint/visitor-keys': 8.67.0
- eslint: 10.8.1(jiti@2.7.0)
- ignore: 7.0.6
+ '@typescript-eslint/parser': 8.70.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.70.0
+ '@typescript-eslint/type-utils': 8.70.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.70.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.70.0
+ eslint: 10.10.0(jiti@2.7.0)
+ ignore: 7.0.9
natural-compare: 1.4.0
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)':
+ '@typescript-eslint/parser@8.70.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/scope-manager': 8.67.0
- '@typescript-eslint/types': 8.67.0
- '@typescript-eslint/typescript-estree': 8.67.0(typescript@6.0.3)
- '@typescript-eslint/visitor-keys': 8.67.0
+ '@typescript-eslint/scope-manager': 8.70.0
+ '@typescript-eslint/types': 8.70.0
+ '@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.70.0
debug: 4.4.3
- eslint: 10.8.1(jiti@2.7.0)
+ eslint: 10.10.0(jiti@2.7.0)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/project-service@8.67.0(typescript@6.0.3)':
+ '@typescript-eslint/project-service@8.70.0(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@6.0.3)
- '@typescript-eslint/types': 8.67.0
+ '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.70.0
debug: 4.4.3
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/scope-manager@8.67.0':
+ '@typescript-eslint/scope-manager@8.70.0':
dependencies:
- '@typescript-eslint/types': 8.67.0
- '@typescript-eslint/visitor-keys': 8.67.0
+ '@typescript-eslint/types': 8.70.0
+ '@typescript-eslint/visitor-keys': 8.70.0
- '@typescript-eslint/tsconfig-utils@8.67.0(typescript@6.0.3)':
+ '@typescript-eslint/tsconfig-utils@8.70.0(typescript@6.0.3)':
dependencies:
typescript: 6.0.3
- '@typescript-eslint/type-utils@8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)':
+ '@typescript-eslint/type-utils@8.70.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/types': 8.67.0
- '@typescript-eslint/typescript-estree': 8.67.0(typescript@6.0.3)
- '@typescript-eslint/utils': 8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/types': 8.70.0
+ '@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.70.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)
debug: 4.4.3
- eslint: 10.8.1(jiti@2.7.0)
+ eslint: 10.10.0(jiti@2.7.0)
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/types@8.67.0': {}
+ '@typescript-eslint/types@8.70.0': {}
- '@typescript-eslint/typescript-estree@8.67.0(typescript@6.0.3)':
+ '@typescript-eslint/typescript-estree@8.70.0(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/project-service': 8.67.0(typescript@6.0.3)
- '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@6.0.3)
- '@typescript-eslint/types': 8.67.0
- '@typescript-eslint/visitor-keys': 8.67.0
+ '@typescript-eslint/project-service': 8.70.0(typescript@6.0.3)
+ '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.70.0
+ '@typescript-eslint/visitor-keys': 8.70.0
debug: 4.4.3
minimatch: 10.2.6
semver: 7.8.5
@@ -6456,20 +6520,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)':
+ '@typescript-eslint/utils@8.70.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)':
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0))
- '@typescript-eslint/scope-manager': 8.67.0
- '@typescript-eslint/types': 8.67.0
- '@typescript-eslint/typescript-estree': 8.67.0(typescript@6.0.3)
- eslint: 10.8.1(jiti@2.7.0)
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0))
+ '@typescript-eslint/scope-manager': 8.70.0
+ '@typescript-eslint/types': 8.70.0
+ '@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3)
+ eslint: 10.10.0(jiti@2.7.0)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/visitor-keys@8.67.0':
+ '@typescript-eslint/visitor-keys@8.70.0':
dependencies:
- '@typescript-eslint/types': 8.67.0
+ '@typescript-eslint/types': 8.70.0
eslint-visitor-keys: 5.0.1
'@ungap/structured-clone@1.3.3': {}
@@ -6553,47 +6617,47 @@ snapshots:
'@types/react': 19.2.18
'@visx/point': 4.0.1-alpha.0
- '@visx/grid@4.0.1-alpha.0(react@19.2.8)':
+ '@visx/grid@4.0.1-alpha.0(react@19.3.0)':
dependencies:
'@types/react': 19.2.18
'@visx/curve': 4.0.1-alpha.0
- '@visx/group': 4.0.1-alpha.0(react@19.2.8)
+ '@visx/group': 4.0.1-alpha.0(react@19.3.0)
'@visx/point': 4.0.1-alpha.0
'@visx/scale': 4.0.1-alpha.0
- '@visx/shape': 4.0.1-alpha.0(react@19.2.8)
+ '@visx/shape': 4.0.1-alpha.0(react@19.3.0)
classnames: 2.5.1
- react: 19.2.8
+ react: 19.3.0
- '@visx/group@4.0.1-alpha.0(react@19.2.8)':
+ '@visx/group@4.0.1-alpha.0(react@19.3.0)':
dependencies:
'@types/react': 19.2.18
classnames: 2.5.1
- react: 19.2.8
+ react: 19.3.0
'@visx/point@4.0.1-alpha.0': {}
- '@visx/responsive@4.0.1-alpha.0(react@19.2.8)':
+ '@visx/responsive@4.0.1-alpha.0(react@19.3.0)':
dependencies:
'@types/lodash': 4.17.25
'@types/react': 19.2.18
lodash: 4.18.1
- react: 19.2.8
+ react: 19.3.0
'@visx/scale@4.0.1-alpha.0':
dependencies:
'@visx/vendor': 4.0.0-alpha.0
- '@visx/shape@4.0.1-alpha.0(react@19.2.8)':
+ '@visx/shape@4.0.1-alpha.0(react@19.3.0)':
dependencies:
'@types/lodash': 4.17.25
'@types/react': 19.2.18
'@visx/curve': 4.0.1-alpha.0
- '@visx/group': 4.0.1-alpha.0(react@19.2.8)
+ '@visx/group': 4.0.1-alpha.0(react@19.3.0)
'@visx/scale': 4.0.1-alpha.0
'@visx/vendor': 4.0.0-alpha.0
classnames: 2.5.1
lodash: 4.18.1
- react: 19.2.8
+ react: 19.3.0
'@visx/vendor@4.0.0-alpha.0':
dependencies:
@@ -6771,15 +6835,15 @@ snapshots:
baseline-browser-mapping@2.11.21: {}
- better-auth@1.7.3(@prisma/client@7.10.0(prisma@7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3))(mysql2@3.24.3(@types/node@26.2.0))(next@16.3.3(@babel/core@7.29.0)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(pg@8.20.0)(prisma@7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
+ better-auth@1.7.3(@prisma/client@7.10.0(prisma@7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(typescript@6.0.3))(mysql2@3.24.3(@types/node@26.6.1))(next@16.3.4(@babel/core@7.29.0)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.20.0)(prisma@7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
dependencies:
- '@better-auth/core': 1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)
- '@better-auth/drizzle-adapter': 1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)
- '@better-auth/kysely-adapter': 1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(kysely@0.29.5)
- '@better-auth/memory-adapter': 1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)
- '@better-auth/mongo-adapter': 1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)
- '@better-auth/prisma-adapter': 1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@prisma/client@7.10.0(prisma@7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3))(prisma@7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))
- '@better-auth/telemetry': 1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)
+ '@better-auth/core': 1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3)
+ '@better-auth/drizzle-adapter': 1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)
+ '@better-auth/kysely-adapter': 1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(kysely@0.29.5)
+ '@better-auth/memory-adapter': 1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)
+ '@better-auth/mongo-adapter': 1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)
+ '@better-auth/prisma-adapter': 1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@prisma/client@7.10.0(prisma@7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(typescript@6.0.3))(prisma@7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))
+ '@better-auth/telemetry': 1.7.3(@better-auth/core@1.7.3(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.5)(nanostores@1.5.3))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)
'@better-auth/utils': 0.4.2
'@better-fetch/fetch': 1.3.1
'@noble/ciphers': 2.4.0
@@ -6791,13 +6855,13 @@ snapshots:
nanostores: 1.5.3
zod: 4.5.4
optionalDependencies:
- '@prisma/client': 7.10.0(prisma@7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(typescript@6.0.3)
- mysql2: 3.24.3(@types/node@26.2.0)
- next: 16.3.3(@babel/core@7.29.0)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ '@prisma/client': 7.10.0(prisma@7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3))(typescript@6.0.3)
+ mysql2: 3.24.3(@types/node@26.6.1)
+ next: 16.3.4(@babel/core@7.29.0)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
pg: 8.20.0
- prisma: 7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3)
- react: 19.2.8
- react-dom: 19.2.8(react@19.2.8)
+ prisma: 7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3)
+ react: 19.3.0
+ react-dom: 19.3.0(react@19.3.0)
transitivePeerDependencies:
- '@cloudflare/workers-types'
- '@opentelemetry/api'
@@ -6889,6 +6953,14 @@ snapshots:
pkg-types: 2.3.3
rc9: 3.1.0
+ cacheable@2.5.0:
+ dependencies:
+ '@cacheable/memory': 2.2.0
+ '@cacheable/utils': 2.5.0
+ hookified: 1.15.1
+ keyv: 5.6.0
+ qified: 0.10.1
+
callsites@3.1.0: {}
camelcase@5.3.1: {}
@@ -7203,14 +7275,14 @@ snapshots:
escape-string-regexp@4.0.0: {}
- eslint-plugin-react-hooks@7.0.1(eslint@10.8.1(jiti@2.7.0)):
+ eslint-plugin-react-hooks@7.0.1(eslint@10.10.0(jiti@2.7.0)):
dependencies:
'@babel/core': 7.29.0
'@babel/parser': 7.29.0
- eslint: 10.8.1(jiti@2.7.0)
+ eslint: 10.10.0(jiti@2.7.0)
hermes-parser: 0.25.1
- zod: 4.4.3
- zod-validation-error: 4.0.2(zod@4.4.3)
+ zod: 4.5.4
+ zod-validation-error: 4.0.2(zod@4.5.4)
transitivePeerDependencies:
- supports-color
@@ -7227,14 +7299,14 @@ snapshots:
eslint-visitor-keys@5.0.1: {}
- eslint@10.8.1(jiti@2.7.0):
+ eslint@10.10.0(jiti@2.7.0):
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0))
+ '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0))
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.23.5
'@eslint/config-helpers': 0.7.0
'@eslint/core': 1.2.1
- '@eslint/plugin-kit': 0.7.2
+ '@eslint/plugin-kit': 0.7.3
'@humanfs/node': 0.16.8
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.4.3
@@ -7249,7 +7321,7 @@ snapshots:
esquery: 1.7.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
- file-entry-cache: 8.0.0
+ file-entry-cache: 11.1.5
find-up: 5.0.0
glob-parent: 6.0.2
ignore: 5.3.2
@@ -7376,15 +7448,15 @@ snapshots:
dependencies:
bser: 2.1.1
- fdir@6.5.0(picomatch@4.0.5):
+ fdir@6.5.0(picomatch@4.0.7):
optionalDependencies:
- picomatch: 4.0.5
+ picomatch: 4.0.7
fflate@0.8.3: {}
- file-entry-cache@8.0.0:
+ file-entry-cache@11.1.5:
dependencies:
- flat-cache: 4.0.1
+ flat-cache: 6.1.23
fill-range@7.1.1:
dependencies:
@@ -7406,10 +7478,11 @@ snapshots:
locate-path: 6.0.0
path-exists: 4.0.0
- flat-cache@4.0.1:
+ flat-cache@6.1.23:
dependencies:
+ cacheable: 2.5.0
flatted: 3.4.4
- keyv: 4.5.4
+ hookified: 1.15.1
flatted@3.4.4: {}
@@ -7484,12 +7557,20 @@ snapshots:
has-flag@4.0.0: {}
+ hashery@1.5.1:
+ dependencies:
+ hookified: 1.15.1
+
hermes-estree@0.25.1: {}
hermes-parser@0.25.1:
dependencies:
hermes-estree: 0.25.1
+ hookified@1.15.1: {}
+
+ hookified@2.2.0: {}
+
html-encoding-sniffer@6.0.0(@noble/hashes@2.4.0):
dependencies:
'@exodus/bytes': 1.15.1(@noble/hashes@2.4.0)
@@ -7531,7 +7612,7 @@ snapshots:
ignore@5.3.2: {}
- ignore@7.0.6: {}
+ ignore@7.0.9: {}
immediate@3.0.6: {}
@@ -7665,7 +7746,7 @@ snapshots:
- babel-plugin-macros
- supports-color
- jest-cli@30.4.2(@types/node@26.2.0):
+ jest-cli@30.4.2(@types/node@26.6.1):
dependencies:
'@jest/core': 30.4.2
'@jest/test-result': 30.4.1
@@ -7673,7 +7754,7 @@ snapshots:
chalk: 4.1.2
exit-x: 0.2.2
import-local: 3.2.0
- jest-config: 30.4.2(@types/node@26.2.0)
+ jest-config: 30.4.2(@types/node@26.6.1)
jest-util: 30.4.1
jest-validate: 30.4.1
yargs: 17.7.3
@@ -7715,6 +7796,37 @@ snapshots:
- babel-plugin-macros
- supports-color
+ jest-config@30.4.2(@types/node@26.6.1):
+ dependencies:
+ '@babel/core': 7.29.0
+ '@jest/get-type': 30.1.0
+ '@jest/pattern': 30.4.0
+ '@jest/test-sequencer': 30.4.1
+ '@jest/types': 30.4.1
+ babel-jest: 30.4.1(@babel/core@7.29.0)
+ chalk: 4.1.2
+ ci-info: 4.4.0
+ deepmerge: 4.3.1
+ glob: 10.5.0
+ graceful-fs: 4.2.11
+ jest-circus: 30.4.2
+ jest-docblock: 30.4.0
+ jest-environment-node: 30.4.1
+ jest-regex-util: 30.4.0
+ jest-resolve: 30.4.1
+ jest-runner: 30.4.2
+ jest-util: 30.4.1
+ jest-validate: 30.4.1
+ parse-json: 5.2.0
+ pretty-format: 30.4.1
+ slash: 3.0.0
+ strip-json-comments: 3.1.1
+ optionalDependencies:
+ '@types/node': 26.6.1
+ transitivePeerDependencies:
+ - babel-plugin-macros
+ - supports-color
+
jest-diff@30.4.1:
dependencies:
'@jest/diff-sequences': 30.4.0
@@ -7931,12 +8043,12 @@ snapshots:
merge-stream: 2.0.0
supports-color: 8.1.1
- jest@30.4.2(@types/node@26.2.0):
+ jest@30.4.2(@types/node@26.6.1):
dependencies:
'@jest/core': 30.4.2
'@jest/types': 30.4.1
import-local: 3.2.0
- jest-cli: 30.4.2(@types/node@26.2.0)
+ jest-cli: 30.4.2(@types/node@26.6.1)
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@@ -7987,8 +8099,6 @@ snapshots:
jsesc@3.1.0: {}
- json-buffer@3.0.1: {}
-
json-parse-even-better-errors@2.3.1: {}
json-schema-traverse@0.4.1: {}
@@ -8017,9 +8127,9 @@ snapshots:
readable-stream: 2.3.8
setimmediate: 1.0.5
- keyv@4.5.4:
+ keyv@5.6.0:
dependencies:
- json-buffer: 3.0.1
+ '@keyv/serialize': 1.1.1
kysely@0.29.5: {}
@@ -8145,9 +8255,9 @@ snapshots:
lru.min@1.1.5: {}
- lucide-react@0.575.0(react@19.2.8):
+ lucide-react@0.575.0(react@19.3.0):
dependencies:
- react: 19.2.8
+ react: 19.3.0
magic-string@0.30.21:
dependencies:
@@ -8204,9 +8314,9 @@ snapshots:
ms@2.1.3: {}
- mysql2@3.24.3(@types/node@26.2.0):
+ mysql2@3.24.3(@types/node@26.6.1):
dependencies:
- '@types/node': 26.2.0
+ '@types/node': 26.6.1
aws-ssl-profiles: 1.1.2
generate-function: 2.3.1
iconv-lite: 0.7.3
@@ -8221,7 +8331,7 @@ snapshots:
nanoid@3.3.17: {}
- nanoid@3.3.18: {}
+ nanoid@3.3.19: {}
nanostores@1.5.3: {}
@@ -8229,26 +8339,26 @@ snapshots:
natural-compare@1.4.0: {}
- next@16.3.3(@babel/core@7.29.0)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
+ next@16.3.4(@babel/core@7.29.0)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
dependencies:
- '@next/env': 16.3.3
+ '@next/env': 16.3.4
'@swc/helpers': 0.5.23
baseline-browser-mapping: 2.11.21
caniuse-lite: 1.0.30001810
postcss: 8.5.23
- react: 19.2.8
- react-dom: 19.2.8(react@19.2.8)
- styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.8)
+ react: 19.3.0
+ react-dom: 19.3.0(react@19.3.0)
+ styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.3.0)
optionalDependencies:
- '@next/swc-darwin-arm64': 16.3.3
- '@next/swc-darwin-x64': 16.3.3
- '@next/swc-linux-arm64-gnu': 16.3.3
- '@next/swc-linux-arm64-musl': 16.3.3
- '@next/swc-linux-x64-gnu': 16.3.3
- '@next/swc-linux-x64-musl': 16.3.3
- '@next/swc-win32-arm64-msvc': 16.3.3
- '@next/swc-win32-x64-msvc': 16.3.3
- sharp: 0.35.4(@types/node@26.2.0)
+ '@next/swc-darwin-arm64': 16.3.4
+ '@next/swc-darwin-x64': 16.3.4
+ '@next/swc-linux-arm64-gnu': 16.3.4
+ '@next/swc-linux-arm64-musl': 16.3.4
+ '@next/swc-linux-x64-gnu': 16.3.4
+ '@next/swc-linux-x64-musl': 16.3.4
+ '@next/swc-win32-arm64-msvc': 16.3.4
+ '@next/swc-win32-x64-msvc': 16.3.4
+ sharp: 0.35.4(@types/node@26.6.1)
transitivePeerDependencies:
- '@babel/core'
- '@types/node'
@@ -8394,7 +8504,7 @@ snapshots:
picomatch@4.0.4: {}
- picomatch@4.0.5: {}
+ picomatch@4.0.7: {}
pirates@4.0.7: {}
@@ -8417,7 +8527,7 @@ snapshots:
postcss@8.5.23:
dependencies:
- nanoid: 3.3.18
+ nanoid: 3.3.19
picocolors: 1.1.1
source-map-js: 1.2.1
@@ -8454,15 +8564,15 @@ snapshots:
'@jest/schemas': 30.4.1
ansi-styles: 5.2.0
react-is-18: react-is@18.3.1
- react-is-19: react-is@19.2.8
+ react-is-19: react-is@19.3.0
- prisma@7.10.0(@types/node@26.2.0)(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3):
+ prisma@7.10.0(@types/node@26.6.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(typescript@6.0.3):
dependencies:
'@prisma/config': 7.10.0
'@prisma/dev': 0.24.17(typescript@6.0.3)
'@prisma/engines': 7.10.0
- '@prisma/studio-core': 0.33.0(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
- mysql2: 3.24.3(@types/node@26.2.0)
+ '@prisma/studio-core': 0.33.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ mysql2: 3.24.3(@types/node@26.6.1)
postgres: 3.4.7
optionalDependencies:
typescript: 6.0.3
@@ -8564,6 +8674,10 @@ snapshots:
pure-rand@7.0.1: {}
+ qified@0.10.1:
+ dependencies:
+ hookified: 2.2.0
+
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
@@ -8582,29 +8696,29 @@ snapshots:
defu: 6.1.7
destr: 2.0.5
- react-dom@19.2.8(react@19.2.8):
+ react-dom@19.3.0(react@19.3.0):
dependencies:
- react: 19.2.8
- scheduler: 0.27.0
+ react: 19.3.0
+ scheduler: 0.28.0
- react-image-crop@11.1.2(react@19.2.8):
+ react-image-crop@11.1.2(react@19.3.0):
dependencies:
- react: 19.2.8
+ react: 19.3.0
react-is@18.3.1: {}
- react-is@19.2.8: {}
+ react-is@19.3.0: {}
- react-redux@9.2.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1):
+ react-redux@9.2.0(@types/react@19.3.0)(react@19.3.0)(redux@5.0.1):
dependencies:
'@types/use-sync-external-store': 0.0.6
- react: 19.2.8
- use-sync-external-store: 1.6.0(react@19.2.8)
+ react: 19.3.0
+ use-sync-external-store: 1.6.0(react@19.3.0)
optionalDependencies:
- '@types/react': 19.2.18
+ '@types/react': 19.3.0
redux: 5.0.1
- react@19.2.8: {}
+ react@19.3.0: {}
readable-stream@2.3.8:
dependencies:
@@ -8628,21 +8742,21 @@ snapshots:
readdirp@5.1.1: {}
- recharts@3.10.1(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1):
+ recharts@3.10.1(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react-is@19.3.0)(react@19.3.0)(redux@5.0.1):
dependencies:
- '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1))(react@19.2.8)
+ '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.3.0)(react@19.3.0)(redux@5.0.1))(react@19.3.0)
clsx: 2.1.1
decimal.js-light: 2.5.1
es-toolkit: 1.44.0
eventemitter3: 5.0.4
immer: 11.1.16
- react: 19.2.8
- react-dom: 19.2.8(react@19.2.8)
- react-is: 19.2.8
- react-redux: 9.2.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1)
+ react: 19.3.0
+ react-dom: 19.3.0(react@19.3.0)
+ react-is: 19.3.0
+ react-redux: 9.2.0(@types/react@19.3.0)(react@19.3.0)(redux@5.0.1)
reselect: 5.2.0
tiny-invariant: 1.3.3
- use-sync-external-store: 1.6.0(react@19.2.8)
+ use-sync-external-store: 1.6.0(react@19.3.0)
victory-vendor: 37.3.6
transitivePeerDependencies:
- '@types/react'
@@ -8716,7 +8830,7 @@ snapshots:
dependencies:
xmlchars: 2.2.0
- scheduler@0.27.0: {}
+ scheduler@0.28.0: {}
selderee@0.11.0:
dependencies:
@@ -8734,7 +8848,7 @@ snapshots:
setimmediate@1.0.5: {}
- sharp@0.35.4(@types/node@26.2.0):
+ sharp@0.35.4(@types/node@26.6.1):
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
@@ -8765,7 +8879,7 @@ snapshots:
'@img/sharp-win32-arm64': 0.35.4
'@img/sharp-win32-ia32': 0.35.4
'@img/sharp-win32-x64': 0.35.4
- '@types/node': 26.2.0
+ '@types/node': 26.6.1
optional: true
shebang-command@2.0.0:
@@ -8839,10 +8953,10 @@ snapshots:
strip-json-comments@3.1.1: {}
- styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.8):
+ styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.3.0):
dependencies:
client-only: 0.0.1
- react: 19.2.8
+ react: 19.3.0
optionalDependencies:
'@babel/core': 7.29.0
@@ -8896,8 +9010,8 @@ snapshots:
tinyglobby@0.2.17:
dependencies:
- fdir: 6.5.0(picomatch@4.0.5)
- picomatch: 4.0.5
+ fdir: 6.5.0(picomatch@4.0.7)
+ picomatch: 4.0.7
tldts-core@7.4.10: {}
@@ -8947,13 +9061,13 @@ snapshots:
type-fest@0.21.3: {}
- typescript-eslint@8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3):
+ typescript-eslint@8.70.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3):
dependencies:
- '@typescript-eslint/eslint-plugin': 8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)
- '@typescript-eslint/parser': 8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)
- '@typescript-eslint/typescript-estree': 8.67.0(typescript@6.0.3)
- '@typescript-eslint/utils': 8.67.0(eslint@10.8.1(jiti@2.7.0))(typescript@6.0.3)
- eslint: 10.8.1(jiti@2.7.0)
+ '@typescript-eslint/eslint-plugin': 8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.70.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)
+ '@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.70.0(eslint@10.10.0(jiti@2.7.0))(typescript@6.0.3)
+ eslint: 10.10.0(jiti@2.7.0)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
@@ -8962,6 +9076,8 @@ snapshots:
undici-types@8.3.0: {}
+ undici-types@8.9.0: {}
+
undici@8.10.0: {}
unrs-resolver@1.12.2:
@@ -9014,9 +9130,9 @@ snapshots:
dependencies:
punycode: 2.3.1
- use-sync-external-store@1.6.0(react@19.2.8):
+ use-sync-external-store@1.6.0(react@19.3.0):
dependencies:
- react: 19.2.8
+ react: 19.3.0
util-deprecate@1.0.2: {}
@@ -9175,10 +9291,10 @@ snapshots:
compress-commons: 4.1.2
readable-stream: 3.6.2
- zod-validation-error@4.0.2(zod@4.4.3):
+ zod-validation-error@4.0.2(zod@4.5.4):
dependencies:
- zod: 4.4.3
-
- zod@4.4.3: {}
+ zod: 4.5.4
zod@4.5.4: {}
+
+ zod@4.6.5: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index ce127c4b..9446fbcd 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -20,19 +20,12 @@ overrides:
'undici@>=7.0.0 <7.29.0': '~7.29.0'
'ws@<8.19.1': '>=8.19.1'
'postcss@<8.5.18': '>=8.5.18'
- # Keep the ProseMirror core single-instance. @tiptap/pm resolved
- # prosemirror-model 1.25.11 / prosemirror-view 1.42.2 while its siblings
- # (prosemirror-commands, -state, -transform, -tables, …) stayed on 1.25.7 /
- # 1.41.8. These packages do instanceof checks on each other's classes, so a
- # second copy makes them fail: pressing Enter in the editor threw "Can not
- # convert <> to a Fragment (looks like multiple versions of prosemirror-model
- # were loaded)" and no new paragraph was inserted.
+ # Keep the ProseMirror core single-instance: its packages do instanceof checks
+ # on each other, so a second copy breaks the editor (e.g. Enter).
'prosemirror-model': '^1.25.11'
'prosemirror-view': '^1.42.2'
- # Vulnerable transitive deps with no reachable parent bump; Dependabot
- # reports these as "no update possible" on every security run because it
- # can only touch direct dependencies.
+ # Vulnerable transitive deps with no reachable parent bump (Dependabot can't fix them).
'js-yaml@<3.15.2': '~3.15.2'
'js-yaml@>=4.0.0 <4.3.2': '~4.3.2'
'dompurify@<3.4.13': '^3.4.13'
diff --git a/prisma.config.ts b/prisma.config.ts
index b6388f8c..23adaf3a 100644
--- a/prisma.config.ts
+++ b/prisma.config.ts
@@ -1,5 +1,3 @@
-// This file was generated by Prisma and assumes you have installed the following:
-// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
diff --git a/prisma/backfill-description-linebreaks.ts b/prisma/backfill-description-linebreaks.ts
new file mode 100644
index 00000000..fbd581d9
--- /dev/null
+++ b/prisma/backfill-description-linebreaks.ts
@@ -0,0 +1,65 @@
+/**
+ * Entfernt harte Zeilenumbrüche aus Termin- und Kursbeschreibungen (Regel: src/lib/description-linebreaks.ts).
+ * Wiederholbar, läuft daher am Startbefehl in deploy/stack.yaml (auf mittwald keine Skripte im Container).
+ * Aufruf: npx tsx prisma/backfill-description-linebreaks.ts [--dry-run]
+ */
+import "dotenv/config";
+import { db } from "@/server/db";
+import { entwirreHarteUmbrueche } from "@/lib/description-linebreaks";
+
+async function main() {
+ const schreiben = !process.argv.includes("--dry-run");
+
+ const events = await db.event.findMany({
+ where: { description: { contains: "\n" } },
+ select: { id: true, title: true, description: true },
+ });
+ const courses = await db.course.findMany({
+ where: { description: { contains: "\n" } },
+ select: { id: true, title: true, description: true },
+ });
+
+ let geaendert = 0;
+
+ for (const [art, eintraege] of [
+ ["Termin", events],
+ ["Kurs", courses],
+ ] as const) {
+ for (const eintrag of eintraege) {
+ const alt = eintrag.description ?? "";
+ const neu = entwirreHarteUmbrueche(alt);
+ if (neu === alt) continue;
+ geaendert++;
+ console.log(`\n=== ${art}: ${eintrag.title} (${eintrag.id})`);
+ console.log(`--- vorher\n${alt}`);
+ console.log(`+++ nachher\n${neu}`);
+ if (!schreiben) continue;
+ await db.$transaction(async (tx) => {
+ if (art === "Termin") {
+ await tx.event.update({
+ where: { id: eintrag.id },
+ data: { description: neu },
+ });
+ } else {
+ await tx.course.update({
+ where: { id: eintrag.id },
+ data: { description: neu },
+ });
+ }
+ });
+ }
+ }
+
+ console.log(
+ `\n${geaendert} von ${events.length + courses.length} Beschreibungen mit Umbrüchen betroffen — ${
+ schreiben ? "geschrieben" : "Probelauf, nichts geschrieben"
+ }`,
+ );
+}
+
+main()
+ .catch((fehler) => {
+ console.error(fehler);
+ process.exit(1);
+ })
+ .finally(() => void db.$disconnect());
diff --git a/prisma/backfill-geocode.ts b/prisma/backfill-geocode.ts
index d0f219c0..8b7badf0 100644
--- a/prisma/backfill-geocode.ts
+++ b/prisma/backfill-geocode.ts
@@ -1,9 +1,5 @@
/**
- * Backfill Script: Geocode Locations Missing Coordinates
- *
- * Finds all Location rows without latitude/longitude and geocodes them via
- * Nominatim, respecting its 1 request/second rate limit.
- *
+ * Geocodes Locations without coordinates via Nominatim (max. 1 request/second).
* Usage: npx tsx prisma/backfill-geocode.ts
*/
import "dotenv/config";
diff --git a/prisma/backfill-phone-format.ts b/prisma/backfill-phone-format.ts
index c50ae515..4f06ad65 100644
--- a/prisma/backfill-phone-format.ts
+++ b/prisma/backfill-phone-format.ts
@@ -1,23 +1,8 @@
/**
- * Backfill Script: Normalise stored phone numbers
- *
- * The chor lists arrived from a dozen different district spreadsheets, so the
- * same number could be written "0176/22994781", "01575-2362265" or
- * "+49 171-9530 373". This rewrites every phone column to its house format;
- * see src/lib/phone-number.ts.
- *
- * Two formats, by who the number belongs to. Organisational contacts are
- * German throughout and are written nationally ("0176 22994781"). Numbers
- * people enter about themselves — their account, a course registration — can
- * be foreign, so those keep a country code ("+49 176 22994781").
- *
- * The routers normalise on write, so this only has work to do for rows that
- * predate them. Both formatters are idempotent and unchanged rows are
- * skipped, so it is safe to rerun; it is wired into the app start command in
- * deploy/stack.yaml.
- *
- * Usage: npx tsx prisma/backfill-phone-format.ts
- * npx tsx prisma/backfill-phone-format.ts --dry-run
+ * Rewrites every phone column to its house format (src/lib/phone-number.ts): organisational
+ * contacts national ("0176 22994781"), numbers people enter about themselves with country code.
+ * Idempotent, runs at app start (deploy/stack.yaml).
+ * Usage: npx tsx prisma/backfill-phone-format.ts [--dry-run]
*/
import "dotenv/config";
import { db } from "@/server/db";
@@ -30,10 +15,7 @@ const DRY_RUN = process.argv.includes("--dry-run");
type Format = "national" | "international";
-/**
- * Every phone column in the schema, with the format it should carry.
- * Keep in sync with schema.prisma — `grep -n "phone\|Phone" prisma/schema.prisma`.
- */
+/** Every phone column in the schema; keep in sync with schema.prisma. */
const PHONE_COLUMNS: Array<{
model: string;
fields: string[];
diff --git a/prisma/backfill-slugs.ts b/prisma/backfill-slugs.ts
index 6d2df7f5..ce79f557 100644
--- a/prisma/backfill-slugs.ts
+++ b/prisma/backfill-slugs.ts
@@ -1,21 +1,7 @@
/**
- * Backfill Script: Generate Slugs for Posts, Ensembles, Events and Courses
- *
- * Fills the `slug` columns added by 20260808000051_add_post_and_ensemble_slug
- * and 20260813120000_add_event_and_course_slug.
- * Rows that already have a slug are left alone, so the script is safe to rerun
- * and safe to run again after new content has been created.
- *
- * Ensemble slugs get the town appended when one is known: chor names like
- * "Posaunenchor der Friedenskirche" repeat across the Rheinland, and the town
- * is both the disambiguator and the term people actually search for.
- *
- * Event and course slugs get the year for the same reason — "Adventskonzert"
- * comes round every December.
- *
- * Duplicates and imports deliberately leave `slug` null (see posts.duplicate),
- * so this script is the thing that eventually names them.
- *
+ * Fills missing slugs on Posts, Ensembles, Events and Courses (safe to rerun). Duplicates and
+ * imports deliberately leave `slug` null. Ensembles get the town (chor names repeat), events
+ * and courses the year ("Adventskonzert" every December).
* Usage: npx tsx prisma/backfill-slugs.ts
*/
import "dotenv/config";
diff --git a/prisma/migrations/20260915120000_registration_groups/migration.sql b/prisma/migrations/20260915120000_registration_groups/migration.sql
new file mode 100644
index 00000000..6b49a2d2
--- /dev/null
+++ b/prisma/migrations/20260915120000_registration_groups/migration.sql
@@ -0,0 +1,5 @@
+-- AlterTable
+ALTER TABLE "CourseRegistration" ADD COLUMN "registrationGroupId" TEXT;
+
+-- CreateIndex
+CREATE INDEX "CourseRegistration_registrationGroupId_idx" ON "CourseRegistration"("registrationGroupId");
diff --git a/prisma/migrations/20260915140000_waitlist_promotion_offers/migration.sql b/prisma/migrations/20260915140000_waitlist_promotion_offers/migration.sql
new file mode 100644
index 00000000..f5e2cd55
--- /dev/null
+++ b/prisma/migrations/20260915140000_waitlist_promotion_offers/migration.sql
@@ -0,0 +1,7 @@
+-- AlterTable
+ALTER TABLE "CourseRegistration" ADD COLUMN "promotionOfferExpiresAt" TIMESTAMP(3),
+ADD COLUMN "promotionOfferPassedSeats" INTEGER,
+ADD COLUMN "promotionOfferReminderSentAt" TIMESTAMP(3);
+
+-- CreateIndex
+CREATE INDEX "CourseRegistration_promotionOfferExpiresAt_idx" ON "CourseRegistration"("promotionOfferExpiresAt");
diff --git a/prisma/migrations/20260917140000_download_file_type_image/migration.sql b/prisma/migrations/20260917140000_download_file_type_image/migration.sql
new file mode 100644
index 00000000..e9fb4f2b
--- /dev/null
+++ b/prisma/migrations/20260917140000_download_file_type_image/migration.sql
@@ -0,0 +1,3 @@
+-- AlterEnum
+-- Bilddateien (JPG, PNG, WebP) als Downloads, vor allem Flyer an Terminen (#309).
+ALTER TYPE "FileType" ADD VALUE 'IMAGE';
diff --git a/prisma/post-migration-setup.ts b/prisma/post-migration-setup.ts
index 7600648e..91e62381 100644
--- a/prisma/post-migration-setup.ts
+++ b/prisma/post-migration-setup.ts
@@ -1,13 +1,6 @@
/**
- * Post-Migration Setup Script
- *
- * This script runs AFTER migrations to ensure:
- * 1. System roles exist (permissions are now hardcoded in codebase)
- * 2. Admin user has Administrator role assigned
- *
- * Usage: pnpm tsx prisma/post-migration-setup.ts [admin-email]
- *
- * If admin-email is not provided, it will try to find the first user or use environment variable ADMIN_EMAIL
+ * Runs after migrations: ensures system roles and Bezirke, assigns the Administrator role.
+ * Usage: pnpm tsx prisma/post-migration-setup.ts [admin-email] (ADMIN_EMAIL takes precedence)
*/
import "dotenv/config";
import { db } from "@/server/db";
@@ -20,19 +13,14 @@ async function main() {
console.log("🚀 Starting post-migration setup...\n");
try {
- // Permissions are now hardcoded in the codebase (src/lib/permissions.ts)
-
- // Step 1: Create system roles
console.log("👥 Step 1: Creating system roles...");
const adminRole = await ensureSystemRolesExist();
console.log(" ✅ System roles ready\n");
- // Step 2: Ensure the hardcoded districts exist
console.log("🗺️ Step 2: Ensuring Bezirke exist...");
await ensureBezirkeExist();
console.log(" ✅ Bezirke ready\n");
- // Step 3: Assign admin role to admin user
if (ADMIN_EMAIL) {
console.log(
`🔐 Step 3: Assigning Administrator role to ${ADMIN_EMAIL}...`,
@@ -56,7 +44,6 @@ async function main() {
}
async function ensureSystemRolesExist() {
- // 1. Administrator Role
const adminRole = await db.role.upsert({
where: { name: "Administrator" },
update: {
@@ -70,10 +57,8 @@ async function ensureSystemRolesExist() {
},
});
- // Assign all permissions to admin (using permission keys directly)
const adminPermissionKeys = Object.values(PERMISSIONS);
- // Remove existing permissions and add all
await db.rolePermission.deleteMany({
where: { roleId: adminRole.id },
});
@@ -91,7 +76,6 @@ async function ensureSystemRolesExist() {
` ✓ Administrator role ready (${adminPermissionKeys.length} permissions)`,
);
- // 2. Delete legacy roles (Landesposaunenwart, Benutzer) if they exist
for (const legacyName of ["Landesposaunenwart", "Benutzer"]) {
const legacyRole = await db.role.findUnique({
where: { name: legacyName },
@@ -106,7 +90,7 @@ async function ensureSystemRolesExist() {
}
}
- // 3. Regionalposaunenwart Role (renamed from Posaunenrat)
+ // Regionalposaunenwart was formerly named Posaunenrat
const existingPosaunenrat = await db.role.findUnique({
where: { name: "Posaunenrat" },
});
@@ -135,10 +119,8 @@ async function ensureSystemRolesExist() {
});
}
- // Freigeben ist bewusst nicht bezirksgebunden: RPWs prüfen wie LPW und Admin
- // für das ganze Werk. Die *_APPROVE-Rechte sind zugleich die Marke, an der
- // der Bezirks-Zuschnitt endet (siehe helpers/district-scope.ts) — ohne sie
- // wäre ein RPW wie ein Obmann auf seinen eigenen Bezirk beschränkt.
+ // Freigeben ist bewusst nicht bezirksgebunden. Die *_APPROVE-Rechte beenden den Bezirks-Zuschnitt
+ // (helpers/district-scope.ts); ohne sie wäre ein RPW wie ein Obmann auf seinen Bezirk beschränkt.
const rpwPermissionKeys = [
PERMISSIONS.EVENTS_CREATE,
PERMISSIONS.EVENTS_EDIT,
@@ -148,9 +130,7 @@ async function ensureSystemRolesExist() {
PERMISSIONS.COURSES_EDIT,
PERMISSIONS.COURSES_APPROVE,
PERMISSIONS.COURSES_VIEW,
- // RPWs entscheiden für ihre eigenen Kurse, ob abgerechnet wird; das
- // Erstellen der Rechnungen selbst läuft über die Kurs-Organisatorenrolle
- // bzw. invoices.generate (LPW/Admin).
+ // Nur die Entscheidung, ob abgerechnet wird; Rechnungen erstellen Organisatoren bzw. invoices.generate.
PERMISSIONS.COURSES_ENABLE_INVOICING,
// Anzahlungen gehören zur selben Entscheidung wie die Rechnungsstellung.
PERMISSIONS.COURSES_ENABLE_DOWN_PAYMENT,
@@ -187,7 +167,6 @@ async function ensureSystemRolesExist() {
` ✓ Regionalposaunenwart role ready (${rpwPermissionKeys.length} permissions)`,
);
- // 4. Obleute Role
const obleuteRole = await db.role.upsert({
where: { name: "Obleute" },
update: {
@@ -246,7 +225,6 @@ async function ensureBezirkeExist() {
}
async function assignAdminRole(email: string, adminRoleId: string) {
- // Find user by email
const user = await db.user.findUnique({
where: { email },
});
@@ -264,7 +242,6 @@ async function assignAdminRole(email: string, adminRoleId: string) {
return;
}
- // Check if user already has admin role
const existingAssignment = await db.userRoleAssignment.findUnique({
where: {
userId_roleId: {
@@ -279,12 +256,6 @@ async function assignAdminRole(email: string, adminRoleId: string) {
return;
}
- // Remove all existing role assignments (optional - comment out if you want to keep other roles)
- // await db.userRoleAssignment.deleteMany({
- // where: { userId: user.id },
- // });
-
- // Assign admin role
await db.userRoleAssignment.create({
data: {
userId: user.id,
@@ -295,10 +266,6 @@ async function assignAdminRole(email: string, adminRoleId: string) {
console.log(` ✓ Assigned Administrator role to ${email}`);
}
-// ============================================================================
-// RUN
-// ============================================================================
-
main()
.then(() => {
db.$disconnect();
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 2e7651cc..969c76c0 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -25,19 +25,14 @@ model User {
profileImageId String?
profileImage Media? @relation("UserProfileImage", fields: [profileImageId], references: [id], onDelete: SetNull)
- // District Role (for users assigned to a district)
- // If bezirkId is set, this field can contain the role name (e.g., "Bezirksobmann", "Bezirksobfrau")
+ // Only with bezirkId, e.g. "Bezirksobmann", "Bezirksobfrau"
districtRoleName String?
- // Bezirk association
- // Users can be assigned to a district with a specific role name
bezirkId String?
- // Bio & Preferences
bio String?
preferences Json?
- // Relations
accounts Account[]
sessions Session[]
twoFactor TwoFactor? @relation("UserTwoFactor")
@@ -58,7 +53,6 @@ model User {
representedEnsembles Ensemble[] @relation("EnsembleRepresentative")
conductedAuswahlChors AuswahlChor[] @relation("AuswahlChorConductor")
- // Organization membership relations
bezirkPersons BezirkPerson[] @relation("BezirkPersonUser")
teamMember TeamMember? @relation("TeamMemberUser")
posaunenratMember PosaunenratMember? @relation("PosaunenratMemberUser")
@@ -78,7 +72,6 @@ model User {
displayUsername String?
- // Custom Roles & Permissions
customRoles UserRoleAssignment[]
userPermissions UserPermission[]
bezirkScopes UserBezirkScope[] @relation("UserBezirkScopes")
@@ -123,7 +116,6 @@ model GameResult {
@@map("game_result")
}
-// --------- Permission System Models ---------
/**
* Custom Role model for flexible role-based permissions
@@ -140,10 +132,8 @@ model Role {
parentRole Role? @relation("RoleHierarchy", fields: [parentRoleId], references: [id], onDelete: SetNull)
childRoles Role[] @relation("RoleHierarchy")
- // Permissions assigned to this role
permissions RolePermission[]
- // Users assigned to this role
users UserRoleAssignment[]
createdAt DateTime @default(now())
@@ -284,7 +274,6 @@ model TwoFactor {
@@map("twoFactor")
}
-// --------- Enums ---------
enum EventEnsembleType {
AUSWAHLCHOR
@@ -453,6 +442,7 @@ enum FileType {
XLSX
ZIP
MP3
+ IMAGE // JPG, PNG oder WebP – z. B. Flyer an Terminen
}
enum ContentStatus {
@@ -463,7 +453,6 @@ enum ContentStatus {
ARCHIVED // No longer active
}
-// --------- Models ---------
model Event {
id String @id @default(uuid())
@@ -478,32 +467,26 @@ model Event {
coverImageId String?
coverImage Media? @relation("EventCoverImage", fields: [coverImageId], references: [id], onDelete: SetNull)
- // Date & Time
eventDate DateTime
duration Int?
cancelled Boolean @default(false)
- // Location - use new Location model
locationId String?
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
- // Category & Classification
category EventCategory
bezirkId String?
districtName String?
- // Performance Details
performingEnsembleType EventEnsembleType?
ensembleId String?
auswahlChorId String?
performingEnsembleName String?
leitung String?
- // Participation
openToParticipants Boolean @default(false)
participationInfo String?
- // Pricing
isFree Boolean @default(true)
priceInfo String?
@@ -512,14 +495,12 @@ model Event {
reviewNotes String?
reviewDate DateTime?
- // Relations
ensemble Ensemble? @relation(fields: [ensembleId], references: [id], onDelete: SetNull)
auswahlChor AuswahlChor? @relation(fields: [auswahlChorId], references: [id], onDelete: SetNull)
bezirk Bezirk? @relation(fields: [bezirkId], references: [id], onDelete: SetNull)
priceOptions EventPriceOption[]
downloads EventDownload[]
- // Meta
createdById String?
createdBy User? @relation("EventCreator", fields: [createdById], references: [id], onDelete: SetNull)
reviewerId String?
@@ -583,20 +564,16 @@ model Course {
imageId String?
image Media? @relation("CourseImage", fields: [imageId], references: [id], onDelete: SetNull)
- // Dates
startDate DateTime
endDate DateTime
- // Location - use new Location model
locationId String?
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
- // Classification
courseType CourseType
targetAudience TargetAudience?
bezirkId String?
- // Registration
registrationOpen Boolean @default(false)
registrationOpensAt DateTime? // When registrations become available (details visible before this)
registrationDeadline DateTime?
@@ -612,7 +589,6 @@ model Course {
maxParticipants Int? // optional hard cap
allowWaitingList Boolean @default(false)
- // Pricing
isFree Boolean @default(false)
priceInfo String?
allowSiblingDiscount Boolean @default(false) // Förderverein sibling discount (20% for siblings after first)
@@ -641,16 +617,13 @@ model Course {
/** Eigener Erstattungshinweis bei DownPaymentRefundPolicy.CUSTOM. */
downPaymentRefundText String?
- // Additional Info
prerequisites String?
whatToBring String?
- // Approval Workflow
status ContentStatus @default(PENDING)
reviewNotes String?
reviewDate DateTime?
- // Relations
collaborators CourseCollaborator[]
guestTeamMembers CourseGuestTeamMember[]
bezirk Bezirk? @relation(fields: [bezirkId], references: [id], onDelete: SetNull)
@@ -660,7 +633,6 @@ model Course {
mails CourseMail[]
invoices Invoice[]
- // Meta
createdById String?
createdBy User? @relation("CourseCreator", fields: [createdById], references: [id], onDelete: SetNull)
reviewerId String?
@@ -755,11 +727,8 @@ model CourseCustomField {
@@index([courseId])
}
-/// Global library of reusable registration field definitions. Adding a library
-/// field to a course copies it into CourseCustomField, so later template edits
-/// never affect existing courses. Templates start private to their creator and
-/// become visible to everyone (isGlobal) once a course containing the field is
-/// approved.
+/// Reusable registration fields, copied into CourseCustomField so template edits never affect courses.
+/// Private to the creator until a course containing the field is approved (isGlobal).
model CourseCustomFieldTemplate {
id String @id @default(uuid())
@@ -786,7 +755,6 @@ model CourseRegistration {
courseId String
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
- // Registrant Info
registrantFirstName String
registrantLastName String
registrantEmail String
@@ -804,7 +772,6 @@ model CourseRegistration {
billingCity String?
billingEmail String?
- // Payment & Status
totalPrice Float
/**
* Gewünschte Zahlungsweise (Bar / Rechnung), sofern der Kurs kostenpflichtig ist.
@@ -812,7 +779,6 @@ model CourseRegistration {
paymentMethod CoursePaymentMethod?
registrationStatus RegistrationStatus @default(CONFIRMED)
- // Sibling Discount
siblingDiscountApplied Boolean @default(false)
siblingDiscountStatus SiblingDiscountStatus @default(NONE)
originalTotalPrice Float? // Price before discount
@@ -834,17 +800,37 @@ model CourseRegistration {
/** Interner Vermerk (Kontoauszug, Absprache mit der Kasse zur Erstattung). */
downPaymentNote String?
- // Invoice
invoiceGenerated Boolean @default(false)
invoiceId String? @unique
invoiceDate DateTime?
- // Relations
+ /**
+ * Teile einer aufgeteilten Anmeldung tragen dieselbe Gruppe: reichten die
+ * freien Plätze nicht für alle, wurden die Gewählten bestätigt und die
+ * übrigen auf die Warteliste gesetzt — als eigene Anmeldungen, weil
+ * Kapazität, Rechnungen und Anzahlung an der Anmeldung hängen. Der
+ * Geschwisterkindrabatt wird über die Gruppe berechnet. null: nicht geteilt.
+ */
+ registrationGroupId String?
+
+ /**
+ * Nachrück-Angebot: Es wurden Plätze frei, die nicht für alle Teilnehmer
+ * dieser wartenden Anmeldung reichen. Bis zu diesem Zeitpunkt wählen die
+ * Anmeldenden, wer nachrückt — so lange hält die Warteliste an.
+ */
+ promotionOfferExpiresAt DateTime?
+ /** Wann das Kursteam an das auslaufende Angebot erinnert wurde. */
+ promotionOfferReminderSentAt DateTime?
+ /**
+ * Angebot abgelehnt oder verfallen, als so viele Plätze nutzbar waren: bis
+ * mehr frei werden, gehen sie an die Nächsten auf der Warteliste.
+ */
+ promotionOfferPassedSeats Int?
+
participants Participant[]
invoices Invoice[]
registrant User? @relation(fields: [registrantId], references: [id], onDelete: SetNull)
- // Meta
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -853,12 +839,12 @@ model CourseRegistration {
@@index([siblingDiscountStatus])
@@index([courseId, registrationStatus])
@@index([registrantId])
+ @@index([registrationGroupId])
+ @@index([promotionOfferExpiresAt])
}
-/// One e-mail an organizer sent to the registrants of a course, archived so
-/// the team can still answer "was the info mail sent, and what did it say?".
-/// Recipient addresses are deliberately not stored — only how many were
-/// reached, so the archive doesn't duplicate registrant data.
+/// Archived e-mail from an organizer to a course's registrants. Recipient addresses are
+/// deliberately not stored, only the count, so registrant data isn't duplicated.
model CourseMail {
id String @id @default(uuid())
courseId String
@@ -899,27 +885,16 @@ model InvoiceCounter {
value Int @default(0)
}
-/// Fortlaufende Rechnungsnummer je Kursnummer, für Kurse mit gesetzter
-/// `Course.courseNumber`. Eigene Tabelle statt eines Zählers auf dem Kurs, weil
-/// der Zähler wie {@link InvoiceCounter} in der Ausstell-Transaktion gesperrt
-/// und bei einem Rollback mit zurückgedreht werden muss.
+/// Fortlaufende Rechnungsnummer je `Course.courseNumber`. Eigene Tabelle, weil der Zähler wie
+/// {@link InvoiceCounter} in der Ausstell-Transaktion gesperrt und beim Rollback zurückgedreht wird.
model CourseInvoiceCounter {
courseNumber String @id
value Int @default(0)
}
-/// Eine erstellte Rechnung zu einer Kursanmeldung.
-///
-/// Der Entwurf wird aus der Anmeldung vorbefüllt, ist danach aber ein
-/// eigenständiges Dokument: Empfänger und Positionen werden hier gespeichert
-/// und frei bearbeitet, damit Sonderfälle (Nachlass, bereits geleistete
-/// Anzahlung, Zuschuss) abgebildet werden können, ohne die Anmeldung zu
-/// verfälschen.
-///
-/// Ab `PUBLISHED` ist der Datensatz eingefroren: die Rechnungsnummer ist
-/// vergeben, das PDF liegt unter `pdfPath` und wird nie überschrieben.
-/// Korrekturen laufen über Storno (`CANCELLED`) plus Nachfolgerechnung, die
-/// über `replacesInvoiceId` auf die stornierte zurückzeigt.
+/// Rechnung zu einer Kursanmeldung; nach dem Vorbefüllen eigenständig editierbar (Nachlass, Anzahlung).
+/// Ab `PUBLISHED` eingefroren: Nummer vergeben, PDF nie überschrieben. Korrekturen per Storno
+/// (`CANCELLED`) plus Nachfolgerechnung, die über `replacesInvoiceId` zurückzeigt.
model Invoice {
id String @id @default(uuid())
@@ -977,10 +952,8 @@ model Invoice {
/// Wann die Rechnung zuletzt per Kursmail verschickt wurde.
mailedAt DateTime?
- /// Zahlungseingang. `paidAt` ist die einzige Wahrheit darüber, ob eine
- /// Rechnung beglichen ist — der Dokumentstatus (DRAFT/PUBLISHED/CANCELLED)
- /// bleibt davon unberührt, weil eine bezahlte Rechnung weiterhin eine
- /// ausgestellte Rechnung ist und §14 UStG das Dokument einfriert.
+ /// Einzige Wahrheit über den Zahlungseingang; der Dokumentstatus bleibt unberührt,
+ /// weil §14 UStG das ausgestellte Dokument einfriert.
paidAt DateTime?
/// Tatsächlich eingegangener Betrag. Weicht bei Teilzahlung von
/// `totalAmount` ab; null bedeutet "voller Betrag".
@@ -1070,11 +1043,9 @@ model Location {
country String? @default("Deutschland")
additionalInfo String? // e.g., "Eingang über den Hinterhof"
- // Coordinates for map display
latitude Float?
longitude Float?
- // Usage tracking
events Event[]
courses Course[]
ensembles Ensemble[]
@@ -1098,13 +1069,11 @@ model Post {
excerpt String?
content String // Rich text/Markdown
- // Media
coverImageId String?
coverImage Media? @relation("PostCoverImage", fields: [coverImageId], references: [id], onDelete: SetNull)
coverImagePositionX Float? // Focal point X position (0-100, percentage)
coverImagePositionY Float? // Focal point Y position (0-100, percentage)
- // Classification
category PostCategory
bezirkId String?
bezirk Bezirk? @relation(fields: [bezirkId], references: [id], onDelete: SetNull)
@@ -1114,15 +1083,12 @@ model Post {
author User? @relation("PostAuthor", fields: [authorId], references: [id], onDelete: SetNull)
authorName String? // Custom author name if not linked to a user
- // Pinning
pinned Boolean @default(false)
- // Approval Workflow
status ContentStatus @default(PENDING)
reviewNotes String?
reviewDate DateTime?
- // Meta
createdById String?
createdBy User? @relation("PostCreator", fields: [createdById], references: [id], onDelete: SetNull)
reviewerId String?
@@ -1147,31 +1113,24 @@ model Ensemble {
name String
description String?
- // External / federation identifier, e.g. "13-01" (Chor-Nr from the
- // Posaunenwerk-wide registry). Unique when set; nullable for chors that
- // don't have one yet.
+ // Chor-Nr from the Posaunenwerk-wide registry, e.g. "13-01"
internalId String? @unique
- // District
bezirkId String?
bezirk Bezirk? @relation(fields: [bezirkId], references: [id], onDelete: SetNull)
- // Image
imageId String?
image Media? @relation("EnsembleImage", fields: [imageId], references: [id], onDelete: SetNull)
- // Location - new relation
locationId String?
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
- // Rehearsal schedules (multiple)
rehearsalSchedules RehearsalSchedule[]
// Legacy fields (deprecated - kept for migration)
rehearsalDay String? // e.g., "Montag", "Dienstag", etc.
rehearsalTime String? // e.g., "19:30", "20:00-21:30"
- // Ensemble-level contact
contactWebsite String?
/**
* JSON array of social links, same shape as TeamMember.socials:
@@ -1195,10 +1154,8 @@ model Ensemble {
representativeEmail String?
representativePhone String?
- // Status
isActive Boolean @default(true)
- // Relations
events Event[]
createdAt DateTime @default(now())
@@ -1228,22 +1185,18 @@ model AuswahlChor {
slug String @unique
subtitle String
- // Details
founded String
members String
description String
- // Styling
color String
colorHex String
imageId String?
image Media? @relation("AuswahlChorImage", fields: [imageId], references: [id], onDelete: SetNull)
- // Application
showApplication Boolean @default(false)
- // Relations
conductorId String?
conductor User? @relation("AuswahlChorConductor", fields: [conductorId], references: [id], onDelete: SetNull)
events Event[]
@@ -1260,7 +1213,6 @@ model Bezirk {
name String // "Bezirk 01 - Unterer Niederrhein"
shortName String // "Unterer Niederrhein"
- // Relations
users User[] @relation("BezirkUsers")
contentScopes UserBezirkScope[]
people BezirkPerson[] // Obleute & Stellvertretung shown publicly
@@ -1287,11 +1239,9 @@ model BezirkPerson {
bezirkId String
bezirk Bezirk @relation(fields: [bezirkId], references: [id], onDelete: Cascade)
- // Optional link to a User account
userId String?
user User? @relation("BezirkPersonUser", fields: [userId], references: [id], onDelete: SetNull)
- // Custom person details
name String?
email String?
phone String?
@@ -1319,12 +1269,10 @@ model BezirkPerson {
model Posaunenwart {
id String @id @default(uuid())
- // Optional link to User (like VorstandMember)
userId String? @unique
user User? @relation("PosaunenwartUser", fields: [userId], references: [id], onDelete: SetNull)
- // Custom person details. Used as-is when no user is linked, and as an
- // override for the linked user's data when both are present.
+ // Used as-is without a linked user, otherwise overrides the user's data.
name String?
email String?
phone String?
@@ -1395,8 +1343,7 @@ model PosaunenwartResponsibility {
model TeamMember {
id String @id @default(uuid())
- // Custom person details. Used as-is when no user is linked, and as an
- // override for the linked user's data when both are present.
+ // Used as-is without a linked user, otherwise overrides the user's data.
name String?
email String?
phone String?
@@ -1405,14 +1352,12 @@ model TeamMember {
imageId String? @unique
image Media? @relation("TeamMemberImage", fields: [imageId], references: [id], onDelete: SetNull)
- // Team-specific info
role String? // "Landesposaunenwart", "Geschäftsführer", etc.
responsibilities Json? // JSON array of responsibilities
socials Json? // JSON array of social links
contactType ContactType?
sortOrder Int @default(0)
- // Optional link to User (team members do not need an account)
userId String? @unique
user User? @relation("TeamMemberUser", fields: [userId], references: [id], onDelete: SetNull)
@@ -1430,13 +1375,11 @@ model VorstandMember {
email String?
phone String?
- // Vorstand-specific info
position String // "Landesobmann", "Stellvertretender Landesobmann", etc.
description String? @db.Text // Detailed role description
color String? // Tailwind color class for UI display
sortOrder Int @default(0)
- // Relations
userId String? @unique
user User? @relation("VorstandMemberUser", fields: [userId], references: [id], onDelete: SetNull)
imageId String? @unique
@@ -1451,8 +1394,7 @@ model VorstandMember {
model PosaunenratMember {
id String @id @default(uuid())
- // Custom person details. Used as-is when no user is linked, and as an
- // override for the linked user's data when both are present.
+ // Used as-is without a linked user, otherwise overrides the user's data.
name String?
email String?
phone String?
@@ -1460,11 +1402,9 @@ model PosaunenratMember {
imageId String?
image Media? @relation("PosaunenratMemberImage", fields: [imageId], references: [id], onDelete: SetNull)
- // Posaunenrat-specific info
role PosaunenratRole
sortOrder Int @default(0)
- // Optional link to User (some Posaunenrat members have accounts)
userId String? @unique
user User? @relation("PosaunenratMemberUser", fields: [userId], references: [id], onDelete: SetNull)
@@ -1478,8 +1418,7 @@ model PosaunenratMember {
model FoerdervereinMember {
id String @id @default(uuid())
- // Custom person details. Used as-is when no user is linked, and as an
- // override for the linked user's data when both are present.
+ // Used as-is without a linked user, otherwise overrides the user's data.
name String?
email String?
phone String?
@@ -1487,14 +1426,12 @@ model FoerdervereinMember {
imageId String?
image Media? @relation("FoerdervereinMemberImage", fields: [imageId], references: [id], onDelete: SetNull)
- // Förderverein-specific info
position String? // "Vorsitzender", "Stellvertreter", "Schatzmeister", etc.
role FoerdervereinRole @default(MITGLIED)
memberSince DateTime?
description String? @db.Text
sortOrder Int @default(0)
- // Optional link to User
userId String? @unique
user User? @relation("FoerdervereinMemberUser", fields: [userId], references: [id], onDelete: SetNull)
@@ -1516,7 +1453,6 @@ model HistoryEvent {
image Media? @relation("HistoryEventImage", fields: [imageId], references: [id], onDelete: SetNull)
imageAlt String?
- // Sorting
sortOrder Int @default(0)
createdAt DateTime @default(now())
@@ -1531,24 +1467,19 @@ model Download {
description String?
category DownloadCategory
- // File Info
fileUrl String
fileType FileType
fileSize Int? // bytes (changed from string to int)
- // Tags
tags Json? // JSON array
- // Visibility & Review
isPublic Boolean @default(true)
status ContentStatus @default(PENDING)
reviewNotes String?
- // Uploaded by
uploadedById String?
uploadedBy User? @relation("DownloadUploadedBy", fields: [uploadedById], references: [id], onDelete: SetNull)
- // Relations
events EventDownload[]
createdAt DateTime @default(now())
@@ -1567,7 +1498,6 @@ model Blaeserheft {
year Int
description String
- // Details
chapters Json? // JSON array
highlights Json? // JSON array
@@ -1575,19 +1505,16 @@ model Blaeserheft {
image Media @relation("BlaeserheftImage", fields: [imageId], references: [id], onDelete: Cascade)
audioSample String?
- // Prices
priceBlaeserheft Float?
priceBeiheft Float?
priceTrompeten Float?
priceCd Float?
- // Availability
availableBlaeserheft Boolean @default(true)
availableBeiheft Boolean @default(true)
availableTrompeten Boolean @default(false)
availableCd Boolean @default(true)
- // Sorting
sortOrder Int @default(0)
createdAt DateTime @default(now())
@@ -1599,18 +1526,15 @@ model Blaeserheft {
model Media {
id String @id @default(uuid())
- // File Info
name String // Original filename
filename String @unique // Stored filename (with timestamp/hash)
url String // Public URL to access file
path String // Storage path
- // File Details
mimeType String // image/jpeg, application/pdf, etc.
size Int // Size in bytes
extension String // jpg, png, pdf, etc.
- // Image-specific (nullable for non-images)
width Int?
height Int?
@@ -1618,23 +1542,19 @@ model Media {
focalPointX Float?
focalPointY Float?
- // Metadata
alt String? // Alt text for images
caption String? // Caption/description
title String? // Title
copyright String? // Optional copyright notice
creator String? // Optional photographer/creator name
- // Organization
folder String? // Folder path (e.g., "events/2025", "news")
tags Json? // JSON array of tags
- // Usage tracking & Review
isPublic Boolean @default(true)
status ContentStatus @default(APPROVED) // Default to APPROVED for backwards compatibility
reviewNotes String?
- // Relations - primary usages (kept explicit)
usersProfile User[] @relation("UserProfileImage")
eventsCover Event[] @relation("EventCoverImage")
postsCover Post[] @relation("PostCoverImage")
@@ -1651,7 +1571,6 @@ model Media {
courseImages Course[] @relation("CourseImage")
homepageCarouselItems HomepageCarouselItem[] @relation("HomepageCarouselMedia")
- // Uploaded by
uploadedById String?
uploadedBy User? @relation("MediaUploadedBy", fields: [uploadedById], references: [id], onDelete: SetNull)
@@ -1665,10 +1584,8 @@ model Media {
@@index([url])
}
-/// Newsletter recipients. `isActive` says the address wants the newsletter;
-/// `confirmedAt` says the address proved it does (double opt-in). Both must
-/// hold before anything is sent — an unconfirmed row is only a pending
-/// sign-up, and anyone can create one for an address they do not own.
+/// Send only if `isActive` (wants it) and `confirmedAt` (double opt-in) both hold:
+/// anyone can create an unconfirmed row for an address they do not own.
model NewsletterSubscriber {
id String @id @default(uuid())
email String @unique
@@ -1680,13 +1597,8 @@ model NewsletterSubscriber {
subscribedAt DateTime @default(now())
unsubscribedAt DateTime?
- /// Nachweis der Einwilligung (Art. 7 Abs. 1 DSGVO): die IP-Adressen von
- /// Anmeldung und Bestätigung sowie die Fassung des Einwilligungstextes, dem
- /// zugestimmt wurde. Die Beweislast liegt beim Versender; ein blosses
- /// `confirmedAt` belegt nur, dass irgendwer geklickt hat.
- ///
- /// Wird bei der Abmeldung geleert: ohne laufende Einwilligung gibt es nichts
- /// mehr zu belegen, und die IP darf dann nicht weiter aufbewahrt werden.
+ /// Einwilligungsnachweis (Art. 7 Abs. 1 DSGVO): IPs von Anmeldung und Bestätigung plus
+ /// Fassung des Einwilligungstextes. Bei Abmeldung leeren, die IP darf nicht bleiben.
signupIp String?
confirmIp String?
consentVersion String?
@@ -1749,9 +1661,8 @@ model PageView {
@@map("page_view")
}
-/// Storage for better-auth's built-in rate limiter (rateLimit.storage =
-/// "database"), so login/reset throttling survives restarts and works
-/// across multiple instances.
+/// better-auth rate limiter storage (rateLimit.storage = "database"), so throttling
+/// survives restarts and works across instances.
model RateLimit {
id String @id
key String?
@@ -1804,10 +1715,8 @@ model Notification {
@@map("notification")
}
-/// Public library of user-created note sets for the games under /spiele.
-/// A set is a named list of spelled written pitches in one clef; each game
-/// decides itself whether/how it can play the set (e.g. Griffe checks
-/// fingering coverage per instrument).
+/// User-created note sets (written pitches in one clef) for the games under /spiele.
+/// Each game decides whether it can play a set (e.g. Griffe checks fingering coverage).
model GameNoteSet {
id String @id @default(uuid())
/// Short URL-safe code used for deep links (?set=...), e.g. "k3f7x9q2".
diff --git a/prisma/seed-data/foerderverein.ts b/prisma/seed-data/foerderverein.ts
index 49d5912b..7da1602e 100644
--- a/prisma/seed-data/foerderverein.ts
+++ b/prisma/seed-data/foerderverein.ts
@@ -1,7 +1,4 @@
export const foerdervereinData = [
- // ============================================================================
- // VORSTAND (Board Members)
- // ============================================================================
{
username: null, // Create user if this person needs login access
name: "Stefan Schmelting",
diff --git a/prisma/seed-data/locations.ts b/prisma/seed-data/locations.ts
index 23410f91..d44b6bc8 100644
--- a/prisma/seed-data/locations.ts
+++ b/prisma/seed-data/locations.ts
@@ -1,5 +1,4 @@
export const locationsData = [
- // Common venues across different cities
{
name: "Gemeindehaus Moers-Zentrum",
street: "Kirchstraße 12",
diff --git a/prisma/seed-data/media.ts b/prisma/seed-data/media.ts
index e0bdf995..fb8060d2 100644
--- a/prisma/seed-data/media.ts
+++ b/prisma/seed-data/media.ts
@@ -1,7 +1,4 @@
export const mediaData = {
- // ============================================================================
- // VORSTAND IMAGES
- // ============================================================================
vorstand: [
{
filename: "schmidt-eggert.jpg",
@@ -35,9 +32,7 @@ export const mediaData = {
},
],
- // ============================================================================
- // POSAUNENRAT IMAGES (for members without user accounts)
- // ============================================================================
+ // For members without user accounts
posaunenrat: [
{
filename: "cyganek.jpg",
@@ -47,11 +42,7 @@ export const mediaData = {
},
],
- // ============================================================================
- // FÖRDERVEREIN IMAGES (examples)
- // ============================================================================
foerderverein: [],
- // Team Members
teamMembers: [
{
filename: "joerg-haeusler.jpg",
@@ -115,7 +106,6 @@ export const mediaData = {
},
],
- // Bezirksobleute
obleute: [
{
filename: "haetzel.jpg",
@@ -210,7 +200,6 @@ export const mediaData = {
},
],
- // Bläserhefte
blaeserhefte: [
{
filename: "nordamerika-2025.jpg",
@@ -262,7 +251,6 @@ export const mediaData = {
},
],
- // AuswahlChöre
auswahlchoere: [
{
filename: "conspirito-1.jpg",
@@ -287,7 +275,6 @@ export const mediaData = {
},
],
- // News/Post placeholders
newsPlaceholders: [
{
filename: "news-placeholder-1.jpg",
diff --git a/prisma/seed-data/permissions.ts b/prisma/seed-data/permissions.ts
index 647ef40c..37584d1c 100644
--- a/prisma/seed-data/permissions.ts
+++ b/prisma/seed-data/permissions.ts
@@ -1,11 +1,6 @@
import { PERMISSION_DEFINITIONS } from "@/lib/permissions";
-/**
- * System Permissions Seed Data
- *
- * These permissions are created as system permissions (isSystem: true)
- * and cannot be deleted or modified via the UI.
- */
+/** System permissions (isSystem) cannot be deleted or modified via the UI. */
export const permissionsData = PERMISSION_DEFINITIONS.map((def) => ({
key: def.key,
name: def.name,
diff --git a/prisma/seed-data/posaunenwarte.ts b/prisma/seed-data/posaunenwarte.ts
index ded0fa97..1ff656e0 100644
--- a/prisma/seed-data/posaunenwarte.ts
+++ b/prisma/seed-data/posaunenwarte.ts
@@ -1,9 +1,5 @@
import { PosaunenwartRoleType } from "~/generated/prisma/client";
-// ============================================================================
-// SEED DATA - ONLY POSAUNENWARTE
-// ============================================================================
-
export const posaunenwarteResponsibilitiesData = [
// Jörg Häusler (LPW) - Responsible for ALL Bezirke
{
@@ -55,13 +51,4 @@ export const posaunenwarteResponsibilitiesData = [
priority: 2,
},
- // Add more RPWs here if you have them in the future
- // Example:
- // {
- // username: "another.rpw",
- // roleType: "RPW" as PosaunenwartRoleType,
- // bezirke: [6, 7, 8],
- // notes: "Regionalposaunenwart für die Bezirke 6, 7 und 8",
- // priority: 2,
- // },
];
diff --git a/prisma/seed-data/users.ts b/prisma/seed-data/users.ts
index 146abb2a..aa85fa44 100644
--- a/prisma/seed-data/users.ts
+++ b/prisma/seed-data/users.ts
@@ -256,9 +256,7 @@ export const usersData = [
obleuteBezirk: null,
},
- // ============================================================================
- // VORSTAND MEMBERS (need user accounts)
- // ============================================================================
+ // Vorstand members (need user accounts)
{
username: "friedemann.schmidt_eggert",
email: "landesobmann@posaunenwerk-rheinland.de",
@@ -293,9 +291,7 @@ export const usersData = [
obleuteBezirk: null,
},
- // ============================================================================
- // TEAM MEMBERS (need user accounts)
- // ============================================================================
+ // Team members (need user accounts)
{
username: "heide.neuland",
email: "heide.neuland@posaunenwerk-rheinland.de",
@@ -330,9 +326,7 @@ export const usersData = [
obleuteBezirk: null,
},
- // ============================================================================
- // POSAUNENRAT MEMBERS (need user accounts)
- // ============================================================================
+ // Posaunenrat members (need user accounts)
{
username: "ulrich.cyganek",
email: "ulrich.cyganek@ekir.de",
diff --git a/prisma/seed-from-backup.ts b/prisma/seed-from-backup.ts
index 01e30c5b..9b42af8a 100644
--- a/prisma/seed-from-backup.ts
+++ b/prisma/seed-from-backup.ts
@@ -1,13 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
-/**
- * Seed Script from Backup Data
- * This seed script uses data extracted from a database backup
- */
+/** Seeds the database from data extracted from a backup. */
import "dotenv/config";
import { CourseCollaboratorRole } from "../generated/prisma/client";
import { db } from "../src/server/db";
-// Import all seed data from backup
import { AuswahlChorSeedData } from "./seed-data-from-backup/AuswahlChor";
import { BezirkSeedData } from "./seed-data-from-backup/Bezirk";
import { BlaeserheftSeedData } from "./seed-data-from-backup/Blaeserheft";
@@ -41,13 +37,7 @@ import { SessionSeedData } from "./seed-data-from-backup/session";
import { VerificationSeedData } from "./seed-data-from-backup/verification";
import { _CourseInstructorsSeedData } from "./seed-data-from-backup/_CourseInstructors";
-// ============================================================================
-// HELPER FUNCTIONS
-// ============================================================================
-
-/**
- * Convert PostgreSQL values to proper TypeScript types
- */
+/** Converts PostgreSQL backup values to TypeScript types. */
function convertValue(value: any, fieldName: string): any {
if (
value === null ||
@@ -214,9 +204,6 @@ function convertValue(value: any, fieldName: string): any {
/** Loose row type after backup field conversion (FK remapping, nulls, etc.). */
type CleanedSeedRow = Record;
-/**
- * Clean and convert a data object
- */
function cleanData>(data: T): CleanedSeedRow {
const cleaned: CleanedSeedRow = {};
for (const [key, value] of Object.entries(data)) {
@@ -225,9 +212,6 @@ function cleanData>(data: T): CleanedSeedRow {
return cleaned;
}
-/**
- * Helper to upsert a record (create or update if exists)
- */
async function upsertRecord & { id: string }>(
model: {
upsert: (args: { where: any; update: any; create: any }) => Promise;
@@ -257,10 +241,6 @@ async function upsertRecord & { id: string }>(
}
}
-// ============================================================================
-// MAIN SEED FUNCTION
-// ============================================================================
-
async function main() {
console.log("🌱 Starting database seed from backup data...");
@@ -276,11 +256,10 @@ async function main() {
}
// 2. Seed Media (needed for many relations)
- // Note: Media.uploadedById references User, but we create Media first
- // and set uploadedById to null, then update it after Users are created
+ // Media.uploadedById references User: null for now, set in step 3.5.
console.log("📸 Creating Media...");
const mediaMap = new Map();
- const mediaUploadedByMap = new Map(); // Store original uploadedById for later update
+ const mediaUploadedByMap = new Map();
for (const data of MediaSeedData) {
const cleaned = cleanData(data);
@@ -293,11 +272,9 @@ async function main() {
else if (typeof cleaned.height === "string")
cleaned.height = parseInt(cleaned.height, 10);
- // Store original uploadedById for later update
if (cleaned.uploadedById) {
mediaUploadedByMap.set(data.id, cleaned.uploadedById as string);
}
- // Set uploadedById to null initially (will update after Users are created)
cleaned.uploadedById = null;
const created = await upsertRecord(db.media, cleaned, "filename");
@@ -419,8 +396,7 @@ async function main() {
cleaned.representativeId = null;
}
- // Migrate legacy ensemble-level contactEmail/contactPhone into the new
- // per-person conductor fields. Mirrors the SQL migration.
+ // Legacy contactEmail/contactPhone → conductor fields, like the SQL migration.
const legacy = cleaned as Record;
if (legacy.contactEmail && !legacy.conductorEmail) {
legacy.conductorEmail = legacy.contactEmail;
@@ -686,7 +662,7 @@ async function main() {
if (cleaned.courseId && coursesMap.has(cleaned.courseId as string)) {
cleaned.courseId = coursesMap.get(cleaned.courseId as string);
} else {
- continue; // Skip if course doesn't exist
+ continue;
}
if (
cleaned.registrantId &&
@@ -943,7 +919,6 @@ async function main() {
) {
continue;
}
- // Ensure isActive is boolean
if (typeof cleaned.isActive !== "boolean") {
const rawActive = cleaned.isActive;
cleaned.isActive =
@@ -974,8 +949,7 @@ async function main() {
}
console.log(` ✓ Created ${SessionSeedData.length} sessions`);
- // Skip TwoFactor seeding - 2FA is disabled for all users after restore
- // The backup codes won't work in the new environment, so we don't seed them
+ // Backup codes wouldn't work in the new environment.
console.log(" ⚠ Skipping TwoFactor records (2FA disabled for all users)");
for (const data of VerificationSeedData) {
diff --git a/prisma/seed-roles.ts b/prisma/seed-roles.ts
index 740156cb..e3b00a00 100644
--- a/prisma/seed-roles.ts
+++ b/prisma/seed-roles.ts
@@ -1,10 +1,5 @@
/**
- * Seed Script for Creating Hierarchical Roles
- *
- * This script creates example roles with hierarchical relationships.
- * For example, you can create a "Course Manager" role with all course permissions,
- * and then create other roles that inherit from it.
- *
+ * Creates example roles with hierarchical relationships.
* Usage: npx tsx prisma/seed-roles.ts
*/
import "dotenv/config";
@@ -16,16 +11,12 @@ async function main() {
console.log("🌱 Starting role seed...");
try {
- // Permissions are now hardcoded in the codebase, no need to seed them
-
- // Create base roles with category-specific permissions
console.log("👥 Creating base roles...");
const courseManagerRole = await createCourseManagerRole();
const eventManagerRole = await createEventManagerRole();
const postManagerRole = await createPostManagerRole();
const organizationManagerRole = await createOrganizationManagerRole();
- // Create a comprehensive admin role that inherits from multiple base roles
console.log("👑 Creating admin role...");
const adminRole = await createAdminRole();
@@ -43,7 +34,6 @@ async function main() {
}
async function createCourseManagerRole() {
- // Course-related permission keys (hardcoded)
const coursePermissionKeys = [
PERMISSIONS.COURSES_CREATE,
PERMISSIONS.COURSES_EDIT,
@@ -65,7 +55,6 @@ async function createCourseManagerRole() {
},
});
- // Update permissions
await db.rolePermission.deleteMany({
where: { roleId: role.id },
});
@@ -85,7 +74,6 @@ async function createCourseManagerRole() {
}
async function createEventManagerRole() {
- // Event-related permission keys (hardcoded)
const eventPermissionKeys = [
PERMISSIONS.EVENTS_CREATE,
PERMISSIONS.EVENTS_EDIT,
@@ -106,7 +94,6 @@ async function createEventManagerRole() {
},
});
- // Update permissions
await db.rolePermission.deleteMany({
where: { roleId: role.id },
});
@@ -126,7 +113,6 @@ async function createEventManagerRole() {
}
async function createPostManagerRole() {
- // Post-related permission keys (hardcoded)
const postPermissionKeys = [
PERMISSIONS.POSTS_CREATE,
PERMISSIONS.POSTS_EDIT,
@@ -147,7 +133,6 @@ async function createPostManagerRole() {
},
});
- // Update permissions
await db.rolePermission.deleteMany({
where: { roleId: role.id },
});
@@ -167,7 +152,6 @@ async function createPostManagerRole() {
}
async function createOrganizationManagerRole() {
- // Organization-related permission keys (hardcoded)
const orgPermissionKeys = [
PERMISSIONS.ORGANIZATION_MANAGE_TEAM,
PERMISSIONS.ORGANIZATION_MANAGE_VORSTAND,
@@ -192,7 +176,6 @@ async function createOrganizationManagerRole() {
},
});
- // Update permissions
await db.rolePermission.deleteMany({
where: { roleId: role.id },
});
@@ -212,28 +195,21 @@ async function createOrganizationManagerRole() {
}
async function createAdminRole() {
- // All permission keys (hardcoded)
const allPermissionKeys = PERMISSION_DEFINITIONS.map((p) => p.key);
- // Create admin role that inherits from the first parent role
- // (In a real scenario, you might want to create a composite role differently)
- // For admin role, we'll add all permissions directly instead of using inheritance
- // (inheritance is demonstrated with the base roles)
+ // Admin gets all permissions directly, without inheritance.
const role = await db.role.upsert({
where: { name: "Admin" },
update: {
description: "Full system access with all permissions",
- // parentRoleId: null, // Admin doesn't inherit - has all permissions directly
},
create: {
name: "Admin",
description: "Full system access with all permissions",
isSystem: false,
- // parentRoleId: null, // Admin doesn't inherit - has all permissions directly
},
});
- // Add all permissions directly to admin role
await db.rolePermission.deleteMany({
where: { roleId: role.id },
});
@@ -252,10 +228,6 @@ async function createAdminRole() {
return role;
}
-// ============================================================================
-// RUN
-// ============================================================================
-
main()
.then(() => {
console.log("\n✨ Done!");
diff --git a/prisma/seed-user.ts b/prisma/seed-user.ts
index 50d64666..9caa08f4 100644
--- a/prisma/seed-user.ts
+++ b/prisma/seed-user.ts
@@ -1,16 +1,12 @@
/**
- * Seed Script for Creating Admin User with Full Permissions
- *
- * This script creates/updates the admin user (lars.lehmann) and grants
- * all available permissions to them.
- *
+ * Creates or updates the admin user and grants all permissions directly.
* Usage: npx tsx prisma/seed-user.ts
*/
import "dotenv/config";
import { db } from "@/server/db";
import { PERMISSION_DEFINITIONS } from "@/lib/permissions";
-// Configuration - can be overridden via environment variables
+// Overridable via environment variables
const ADMIN_EMAIL =
process.env.ADMIN_EMAIL || "lars.lehmann@posaunenwerk-rheinland.de";
const ADMIN_USERNAME = process.env.ADMIN_USERNAME || "lars.lehmann";
@@ -20,13 +16,9 @@ async function main() {
console.log("🌱 Starting user seed...");
try {
- // Permissions are now hardcoded in the codebase, no need to seed them
-
- // Create or update the admin user
console.log(`👤 Creating/updating user: ${ADMIN_USERNAME}...`);
const user = await createOrUpdateUser();
- // Grant all permissions to the user
console.log("🔑 Granting all permissions to user...");
await grantAllPermissions(user.id);
@@ -41,7 +33,6 @@ async function main() {
}
async function createOrUpdateUser() {
- // Try to find existing user by email or username
const existingUser =
(await db.user.findUnique({
where: { email: ADMIN_EMAIL },
@@ -52,7 +43,6 @@ async function createOrUpdateUser() {
if (existingUser) {
console.log(` ✓ Found existing user: ${existingUser.email}`);
- // Update user if needed
const updated = await db.user.update({
where: { id: existingUser.id },
data: {
@@ -65,7 +55,6 @@ async function createOrUpdateUser() {
return updated;
}
- // Create new user
const newUser = await db.user.create({
data: {
email: ADMIN_EMAIL,
@@ -79,7 +68,6 @@ async function createOrUpdateUser() {
}
async function grantAllPermissions(userId: string) {
- // Get all permission keys from hardcoded definitions
const allPermissionKeys = PERMISSION_DEFINITIONS.map((p) => p.key);
if (allPermissionKeys.length === 0) {
@@ -87,19 +75,16 @@ async function grantAllPermissions(userId: string) {
return;
}
- // Remove existing permissions for this user (clean slate)
await db.userPermission.deleteMany({
where: { userId },
});
- // Grant all permissions
const permissionGrants = allPermissionKeys.map((permissionKey) => ({
userId,
permissionKey,
granted: true,
}));
- // Batch create all permission grants
await db.userPermission.createMany({
data: permissionGrants,
skipDuplicates: true,
@@ -108,10 +93,6 @@ async function grantAllPermissions(userId: string) {
console.log(` ✓ Granted ${permissionGrants.length} permissions to user`);
}
-// ============================================================================
-// RUN
-// ============================================================================
-
main()
.then(() => {
console.log("\n✨ Done!");
diff --git a/prisma/seed.ts b/prisma/seed.ts
index 5faa33c1..1e97d4ee 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -1,9 +1,4 @@
-/**
- * Seed Script for Posaunenwerk Database
- *
- * Permissions are now hardcoded in the codebase (src/lib/permissions.ts).
- * This seed script is kept for backwards compatibility but does nothing.
- */
+/** No-op, kept for compatibility; permissions live in src/lib/permissions.ts. */
import "dotenv/config";
import { db } from "@/server/db";
@@ -15,10 +10,6 @@ async function main() {
console.log("✅ Seed completed successfully!");
}
-// ============================================================================
-// RUN
-// ============================================================================
-
main()
.then(() => db.$disconnect())
.catch((e) => {
diff --git a/public/images/logo-icon-ink.png b/public/images/logo-icon-ink.png
new file mode 100644
index 00000000..e89d0983
Binary files /dev/null and b/public/images/logo-icon-ink.png differ
diff --git a/public/sw.js b/public/sw.js
index 6e6c8326..a2c0e771 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -1,16 +1,7 @@
/**
- * Service Worker — Offline-Unterstützung, primär für die Spiele unter /spiele.
- *
- * Strategien:
- * - Dokumente (Navigationen) & RSC-Flight-Requests: network-first → Cache →
- * /offline. Online-Nutzer sehen nach einem Deploy immer frisches HTML.
- * - /_next/static & Icons: cache-first (Inhalte sind content-gehasht und
- * unveränderlich), mit LRU-Begrenzung.
- * - /api, /uploads: niemals cachen (personalisiert bzw. dynamisch).
- *
- * Entwicklung: registriert wird nur im Production-Build. Hängt lokal trotzdem
- * ein alter SW fest → in Chrome unter chrome://serviceworker-internals
- * deregistrieren.
+ * Offline-Unterstützung, primär für /spiele. Dokumente & RSC: network-first → Cache → /offline
+ * (nach Deploy frisches HTML); /_next/static & Icons: cache-first mit LRU; /api, /uploads: nie.
+ * Nur im Production-Build registriert; alten SW lokal über chrome://serviceworker-internals entfernen.
*/
const VERSION = "v1";
diff --git a/scripts/backup-db.sh b/scripts/backup-db.sh
index ccf23845..f52dee36 100644
--- a/scripts/backup-db.sh
+++ b/scripts/backup-db.sh
@@ -1,40 +1,28 @@
#!/bin/sh
-# =============================================================================
-# Database Backup Script
-# =============================================================================
-# Creates a compressed PostgreSQL backup using pg_dump
-# Usage: ./backup-db.sh [backup-dir]
-# =============================================================================
+# Compressed pg_dump backup. Usage: ./backup-db.sh [backup-dir]
set -e
-# Default backup directory
BACKUP_DIR="${1:-/backups}"
-# Get database connection details from environment
DB_HOST="${POSTGRES_HOST:-posaunenwerk-db}"
DB_PORT="${POSTGRES_PORT:-5432}"
DB_NAME="${POSTGRES_DB:-posaunenwerk}"
DB_USER="${POSTGRES_USER:-postgres}"
DB_PASSWORD="${POSTGRES_PASSWORD}"
-# Validate required environment variables
if [ -z "$DB_PASSWORD" ]; then
echo "ERROR: POSTGRES_PASSWORD environment variable is required"
exit 1
fi
-# Create backup directory if it doesn't exist
mkdir -p "$BACKUP_DIR"
-# Generate backup filename with timestamp
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
BACKUP_FILE="$BACKUP_DIR/posaunenwerk_backup_${TIMESTAMP}.sql.gz"
-# Export password for pg_dump
export PGPASSWORD="$DB_PASSWORD"
-# Perform the backup
echo "Starting database backup..."
echo "Database: $DB_NAME"
echo "Host: $DB_HOST:$DB_PORT"
@@ -52,7 +40,6 @@ if pg_dump \
| gzip > "$BACKUP_FILE"; then
echo "✓ Backup completed successfully: $BACKUP_FILE"
- # Get file size
FILE_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
echo " Backup size: $FILE_SIZE"
diff --git a/scripts/backup-scheduler.sh b/scripts/backup-scheduler.sh
index e2b8d804..46503de6 100644
--- a/scripts/backup-scheduler.sh
+++ b/scripts/backup-scheduler.sh
@@ -1,33 +1,23 @@
#!/bin/sh
-# =============================================================================
-# Backup Scheduler Entry Point
-# =============================================================================
-# This script runs the backup on a schedule using cron
-# =============================================================================
+# Runs backup-db.sh on BACKUP_SCHEDULE via cron.
BACKUP_DIR="${BACKUP_DIR:-/backups}"
BACKUP_SCHEDULE="${BACKUP_SCHEDULE:-0 2 * * *}" # Default: 2 AM daily
-# Create backup directory
mkdir -p "$BACKUP_DIR"
-# Create log directory
mkdir -p /var/log
-# Create cron job
echo "Setting up scheduled backups..."
echo "Schedule: $BACKUP_SCHEDULE"
echo "Backup directory: $BACKUP_DIR"
-# Write cron job to crontab
echo "$BACKUP_SCHEDULE /scripts/backup-db.sh $BACKUP_DIR >> /var/log/backup.log 2>&1" > /tmp/crontab
crontab /tmp/crontab
rm /tmp/crontab
-# Verify cron job was set
echo "Cron job configured:"
crontab -l
-# Start cron daemon in foreground
echo "Starting cron daemon..."
exec crond -f -l 2
diff --git a/scripts/cron-registration-closed-scheduler.sh b/scripts/cron-registration-closed-scheduler.sh
index c4af606d..e73ed9fe 100755
--- a/scripts/cron-registration-closed-scheduler.sh
+++ b/scripts/cron-registration-closed-scheduler.sh
@@ -1,7 +1,5 @@
#!/bin/sh
-# =============================================================================
-# Registration-closed notification cron (calls app HTTP endpoint)
-# =============================================================================
+# Registration-closed notification cron (calls the app's HTTP endpoint)
set -eu
diff --git a/scripts/fix-migration-checksum.sh b/scripts/fix-migration-checksum.sh
index 780ebbe8..a077a593 100755
--- a/scripts/fix-migration-checksum.sh
+++ b/scripts/fix-migration-checksum.sh
@@ -1,27 +1,7 @@
#!/usr/bin/env bash
-# =============================================================================
-# Fix Migration Checksum Script (Bash / macOS / Linux)
-# =============================================================================
-# Updates Prisma migration checksum to match the current migration file content.
-#
-# Usage:
-# ./scripts/fix-migration-checksum.sh
-# bash scripts/fix-migration-checksum.sh
-#
-# Example:
-# ./scripts/fix-migration-checksum.sh 20260201120000_add_media_copyright_creator
-#
-# Requires:
-# - psql (PostgreSQL client)
-# - openssl (for SHA-256; common on macOS/Linux)
-#
-# Env (same defaults as fix-migration-checksum.ps1):
-# POSTGRES_HOST (default: localhost)
-# POSTGRES_PORT (default: 5432)
-# POSTGRES_DB (default: posaunenwerk)
-# POSTGRES_USER (default: postgres)
-# POSTGRES_PASSWORD (required)
-# =============================================================================
+# Updates a Prisma migration checksum to match the current migration file (needs psql, openssl).
+# Usage: ./scripts/fix-migration-checksum.sh
+# Env: POSTGRES_HOST/PORT/DB/USER (default localhost/5432/posaunenwerk/postgres), POSTGRES_PASSWORD required
set -euo pipefail
diff --git a/scripts/generate-licenses.ts b/scripts/generate-licenses.ts
index efa4761c..3734512b 100644
--- a/scripts/generate-licenses.ts
+++ b/scripts/generate-licenses.ts
@@ -1,17 +1,7 @@
/**
- * Regenerates src/lib/licenses.generated.ts from the packages we depend on
- * directly, i.e. the ones listed in our own package.json.
- *
- * The licence page used to be a hand-maintained list in the Impressum. It named
- * 15 packages while 45 were shipping — a list that goes stale silently is worse
- * than none, so it is derived now and runs as part of `pnpm build`.
- *
- * Each package's own copyright line comes along: MIT, ISC and BSD all require
- * the notice to travel with the software, and that notice differs per package
- * while the licence body does not.
- *
- * Fails soft on purpose: a build must not break because `pnpm licenses` could
- * not run. The previously generated (and committed) file then stays in place.
+ * Regenerates src/lib/licenses.generated.ts from our direct dependencies on every `pnpm build`.
+ * Keeps each package's copyright line: MIT, ISC and BSD require the notice to travel along.
+ * Fails soft: a build must not break because `pnpm licenses` failed; the committed file stays.
*/
import { execFileSync } from "child_process";
import { readdirSync, readFileSync, statSync, writeFileSync } from "fs";
@@ -46,11 +36,7 @@ function directDependencies(): Set {
return new Set(Object.keys(pkg.dependencies ?? {}));
}
-/**
- * The copyright line out of a package's own LICENSE file. Apache-2.0 packages
- * usually ship the bare licence with no such line — null is the right answer
- * there, not a fabricated notice.
- */
+/** Copyright line from the package's LICENSE; null (never a made-up notice) for bare Apache-2.0. */
function copyrightNotice(packagePath: string | undefined): string | null {
if (!packagePath) return null;
try {
diff --git a/scripts/preview-emails.ts b/scripts/preview-emails.ts
new file mode 100644
index 00000000..e90dca2d
--- /dev/null
+++ b/scripts/preview-emails.ts
@@ -0,0 +1,416 @@
+/**
+ * Rendert E-Mail-Vorlagen lokal nach HTML, damit man sie ansehen kann.
+ * Nur für die Entwicklung. pnpm exec tsx scripts/preview-emails.ts
+ */
+import { createElement } from "react";
+import { render } from "@react-email/components";
+import { writeFileSync, mkdirSync } from "fs";
+import {
+ CourseRegistrationConfirmed,
+ courseRegistrationConfirmedText,
+} from "../src/server/email/templates/course-registration-confirmed";
+import {
+ CourseRegistrationWaitlist,
+ courseRegistrationWaitlistText,
+} from "../src/server/email/templates/course-registration-waitlist";
+import {
+ CourseRegistrationCancelled,
+ courseRegistrationCancelledText,
+} from "../src/server/email/templates/course-registration-cancelled";
+import {
+ CourseRegistrationPendingDiscount,
+ courseRegistrationPendingDiscountText,
+} from "../src/server/email/templates/course-registration-pending-discount";
+import {
+ VerificationEmail,
+ verificationEmailText,
+} from "../src/server/email/templates/verification-email";
+import {
+ NewsletterConfirm,
+ newsletterConfirmText,
+} from "../src/server/email/templates/newsletter-confirm";
+import { PasswordResetEmail } from "../src/server/email/templates/password-reset-email";
+import {
+ NewsletterEmail,
+ newsletterEmailText,
+} from "../src/server/email/templates/newsletter";
+import {
+ ContactMessage,
+ contactMessageText,
+} from "../src/server/email/templates/contact-message";
+import {
+ ContentReviewResult,
+ contentReviewResultText,
+} from "../src/server/email/templates/content-review-result";
+import {
+ generateNewsletterHtml,
+ generateNewsletterText,
+} from "../src/server/email/templates/newsletter-html";
+import {
+ generateCourseMailHtml,
+ generateCourseMailText,
+} from "../src/server/email/templates/course-mail-html";
+import {
+ SiblingDiscountApproved,
+ siblingDiscountApprovedText,
+} from "../src/server/email/templates/sibling-discount-approved";
+import {
+ SiblingDiscountRejected,
+ siblingDiscountRejectedText,
+} from "../src/server/email/templates/sibling-discount-rejected";
+import {
+ RegistrationAccessLinks,
+ registrationAccessLinksText,
+} from "../src/server/email/templates/registration-access-links";
+import {
+ CourseRegistrationClosedOverview,
+ courseRegistrationClosedOverviewText,
+} from "../src/server/email/templates/course-registration-closed-overview";
+import type { DownPaymentMailInfo } from "../src/server/email/down-payment";
+import type { CourseRegistrationStats } from "../src/lib/course-participants-export";
+
+const OUT =
+ "/private/tmp/claude-501/-Users-lars-Develompent-pwr-web/37912ebe-1f84-4649-8d3d-671da6870c0a/scratchpad/mails";
+mkdirSync(OUT, { recursive: true });
+
+/** Anzahlung noch offen, jetzt fällig — zeigt Bankdaten und (Platzhalter-)GiroCode. */
+const downPaymentFaellig: DownPaymentMailInfo = {
+ amount: 50,
+ reference: "K-2026-10 Test, Claude",
+ refundNotice:
+ "Bei Stornierung bis 4 Wochen vor Kursbeginn erstatten wir die Anzahlung vollständig.",
+ dueNow: true,
+ alreadyPaid: false,
+ beneficiary: "Posaunenwerk der Evangelischen Kirche im Rheinland e.V.",
+ iban: "DE12 3456 7890 1234 5678 90",
+ bic: "GENODED1XXX",
+ bankName: "KD-Bank",
+};
+
+/** Anzahlung noch nicht fällig — Warteliste, Betrag steht, Bankdaten fehlen noch. */
+const downPaymentNichtFaellig: DownPaymentMailInfo = {
+ ...downPaymentFaellig,
+ dueNow: false,
+};
+
+/** Anzahlung schon eingegangen — z. B. vom Team bei der Erfassung verbucht. */
+const downPaymentBezahlt: DownPaymentMailInfo = {
+ ...downPaymentFaellig,
+ alreadyPaid: true,
+};
+
+const vorlagen = [
+ [
+ "anmeldung-bestaetigt",
+ CourseRegistrationConfirmed,
+ courseRegistrationConfirmedText,
+ {
+ registrantFirstName: "Claude",
+ registrantLastName: "Test",
+ courseTitle: "Lehrgang für Jungbläser*innen",
+ startDate: new Date("2026-10-02"),
+ endDate: new Date("2026-10-05"),
+ totalPrice: 155,
+ participantsCount: 2,
+ registrationId: "31352115-b789-4bb7-b9ee-938ce1414a71",
+ manageUrl: "https://example.org/anmeldung-verwalten?token=abc",
+ downPayment: downPaymentFaellig,
+ downPaymentHasQr: true,
+ },
+ ],
+ [
+ "auf-warteliste",
+ CourseRegistrationWaitlist,
+ courseRegistrationWaitlistText,
+ {
+ registrantFirstName: "Claude",
+ registrantLastName: "Test",
+ courseTitle: "Lehrgang für Jungbläser*innen",
+ startDate: new Date("2026-10-02"),
+ endDate: new Date("2026-10-05"),
+ totalPrice: 155,
+ participantsCount: 2,
+ registrationId: "31352115-b789-4bb7-b9ee-938ce1414a71",
+ manageUrl: "https://example.org/anmeldung-verwalten?token=abc",
+ downPayment: downPaymentNichtFaellig,
+ },
+ ],
+ [
+ "anmeldung-storniert",
+ CourseRegistrationCancelled,
+ courseRegistrationCancelledText,
+ {
+ registrantFirstName: "Claude",
+ registrantLastName: "Test",
+ courseTitle: "Lehrgang für Jungbläser*innen",
+ startDate: new Date("2026-10-02"),
+ endDate: new Date("2026-10-05"),
+ participantsCount: 2,
+ registrationId: "31352115-b789-4bb7-b9ee-938ce1414a71",
+ },
+ ],
+ [
+ "anmeldung-rabatt-pruefung",
+ CourseRegistrationPendingDiscount,
+ courseRegistrationPendingDiscountText,
+ {
+ registrantFirstName: "Claude",
+ registrantLastName: "Test",
+ courseTitle: "Lehrgang für Jungbläser*innen",
+ startDate: new Date("2026-10-02"),
+ endDate: new Date("2026-10-05"),
+ originalTotalPrice: 310,
+ discountAmount: 62,
+ finalTotalPrice: 248,
+ participantsCount: 2,
+ registrationId: "31352115-b789-4bb7-b9ee-938ce1414a71",
+ manageUrl: "https://example.org/anmeldung-verwalten?token=abc",
+ downPayment: downPaymentBezahlt,
+ downPaymentHasQr: false,
+ },
+ ],
+ [
+ "bestaetigung",
+ VerificationEmail,
+ verificationEmailText,
+ {
+ verificationUrl: "https://example.org/verify?token=abc",
+ userName: "Claude Test",
+ },
+ ],
+ [
+ "newsletter-bestaetigen",
+ NewsletterConfirm,
+ newsletterConfirmText,
+ {
+ confirmUrl: "https://example.org/newsletter/bestaetigen?token=abc",
+ subscriberName: "Claude",
+ },
+ ],
+ [
+ "passwort-zuruecksetzen",
+ PasswordResetEmail,
+ null,
+ {
+ resetUrl: "https://example.org/reset-password?token=abc",
+ userName: "Claude Test",
+ },
+ ],
+ [
+ "geschwisterkindrabatt-genehmigt",
+ SiblingDiscountApproved,
+ siblingDiscountApprovedText,
+ {
+ registrantFirstName: "Claude",
+ registrantLastName: "Test",
+ courseTitle: "Lehrgang für Jungbläser*innen",
+ startDate: new Date("2026-10-02"),
+ endDate: new Date("2026-10-05"),
+ originalTotalPrice: 310,
+ discountAmount: 62,
+ finalTotalPrice: 248,
+ participantsCount: 2,
+ registrationId: "31352115-b789-4bb7-b9ee-938ce1414a71",
+ manageUrl: "https://example.org/anmeldung-verwalten?token=abc",
+ },
+ ],
+ [
+ "geschwisterkindrabatt-abgelehnt",
+ SiblingDiscountRejected,
+ siblingDiscountRejectedText,
+ {
+ registrantFirstName: "Claude",
+ registrantLastName: "Test",
+ courseTitle: "Lehrgang für Jungbläser*innen",
+ startDate: new Date("2026-10-02"),
+ endDate: new Date("2026-10-05"),
+ originalTotalPrice: 310,
+ participantsCount: 2,
+ registrationId: "31352115-b789-4bb7-b9ee-938ce1414a71",
+ manageUrl: "https://example.org/anmeldung-verwalten?token=abc",
+ },
+ ],
+ [
+ "anmeldungen-zugang",
+ RegistrationAccessLinks,
+ registrationAccessLinksText,
+ {
+ registrantFirstName: "Claude",
+ registrations: [
+ {
+ courseTitle: "Lehrgang für Jungbläser*innen",
+ startDate: new Date("2026-10-02"),
+ endDate: new Date("2026-10-05"),
+ statusLabel: "Bestätigt",
+ participantsCount: 2,
+ manageUrl: "https://example.org/anmeldung-verwalten?token=abc1",
+ },
+ {
+ courseTitle: "Bläserfreizeit Herbst",
+ startDate: new Date("2026-11-06"),
+ endDate: new Date("2026-11-08"),
+ statusLabel: "Warteliste",
+ participantsCount: 1,
+ manageUrl: "https://example.org/anmeldung-verwalten?token=abc2",
+ },
+ ],
+ },
+ ],
+ [
+ "anmeldefrist-beendet",
+ CourseRegistrationClosedOverview,
+ courseRegistrationClosedOverviewText,
+ {
+ courseTitle: "Lehrgang für Jungbläser*innen",
+ registrationDeadline: new Date("2026-09-20T18:00:00"),
+ startDate: new Date("2026-10-02"),
+ endDate: new Date("2026-10-05"),
+ locationName: "Freizeitheim Vallendar",
+ maxParticipants: 40,
+ allowWaitingList: true,
+ stats: {
+ confirmedParticipants: 34,
+ waitlistParticipants: 6,
+ cancelledParticipants: 2,
+ activeRegistrations: 20,
+ pendingDiscountRegistrations: 3,
+ totalRevenueConfirmed: 5270,
+ paidRevenue: 3100,
+ downPaymentsReceived: 1200,
+ downPaymentsOpen: 400,
+ refundPendingRegistrations: 1,
+ } satisfies CourseRegistrationStats,
+ participantsUrl: "https://example.org/dashboard/kurse/abc/teilnehmer",
+ },
+ ],
+ [
+ "newsletter",
+ NewsletterEmail,
+ newsletterEmailText,
+ {
+ content:
+ "
Liebe Bläserinnen und Bläser,
der Herbstlehrgang ist ausgebucht — die Warteliste läuft noch bis Ende der Woche.