chore: code cleanup, TypeScript fixes, and dependency updates

- Remove unused variables and imports across API routes and workers
- Fix TypeScript errors in ConsentLogs.ts (PayloadRequest header access)
- Fix TypeScript errors in formSubmissionHooks.ts (add ResponseTracking interface)
- Update eslint ignores for coverage, test results, and generated files
- Set push: false in payload.config.ts (schema changes only via migrations)
- Update dependencies to latest versions (Payload 3.68.4, React 19.2.3)
- Add framework update check script and documentation
- Regenerate payload-types.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Martin Porwoll 2025-12-15 09:02:58 +00:00
parent 3b3440cae5
commit 2faefdac1e
22 changed files with 1561 additions and 1199 deletions

View file

@ -21,6 +21,7 @@
| [x] | Staging-Deployment | DevOps | | [x] | Staging-Deployment | DevOps |
| [x] | Memory-Problem lösen (Swap) | Infrastruktur | | [x] | Memory-Problem lösen (Swap) | Infrastruktur |
| [ ] | PM2 Cluster Mode testen | Infrastruktur | | [ ] | PM2 Cluster Mode testen | Infrastruktur |
| [ ] | Payload/Next Releases auf Next.js 16 Support beobachten *(siehe `framework-monitoring.md`)* | Tech Debt |
### Niedrige Priorität ### Niedrige Priorität
| Status | Task | Bereich | | Status | Task | Bereich |

View file

@ -0,0 +1,33 @@
# Framework Monitoring Next.js & Payload
Dieser Leitfaden beschreibt, wie wir beobachten, wann Payload offiziell Next.js 16 (oder spätere) Versionen unterstützt und wann wir die Upgrades wieder aufnehmen können.
## 1. Wöchentlicher Versions-Check
```
pnpm check:frameworks
```
Der Befehl führt `pnpm outdated` nur für Payload-Core und alle Payload-Plugins sowie Next.js aus. Damit sehen wir sofort, ob es neue Veröffentlichungen gibt, die wir evaluieren sollten.
> Falls du den Check auf CI ausführen möchtest, stelle sicher, dass `pnpm` installiert ist und das Repository bereits `pnpm install` ausgeführt hat.
## 2. Release Notes verfolgen
- Payload Releases: https://github.com/payloadcms/payload/releases
Abonniere die Repo-Releases („Watch → Releases only“), damit du automatisch benachrichtigt wirst, wenn ein neues Release Next.js 16 als kompatibel markiert.
- Next.js Blog: https://nextjs.org/blog
Relevant, um Breaking Changes zu erkennen, die Payload evtl. erst später unterstützt.
## 3. Vorgehen bei neuem Payload-Release
1. `pnpm check:frameworks` ausführen und prüfen, ob `@payloadcms/next` oder `@payloadcms/ui` eine neue Version anbieten, deren Peer-Dependencies `next@16` erlauben.
2. Falls ja:
- Branch erstellen (`feature/upgrade-next16`)
- `package.json` anpassen (Next.js + Payload) und `pnpm install`
- `pnpm lint`, `pnpm typecheck`, `pnpm test` und ein Test-Build (`pnpm build && pnpm test:e2e` falls vorhanden) ausführen.
3. Läuft alles fehlerfrei, kann das Update über PR/Merge in `develop`.
## 4. Erinnerung
In der To-Do-Liste (`docs/anleitungen/TODO.md`) gibt es einen Eintrag „Payload/Next Releases auf Next.js 16 Support beobachten“. Wenn das Upgrade abgeschlossen ist, kann dieser Task auf erledigt gesetzt werden.

View file

@ -41,6 +41,11 @@ const eslintConfig = [
{ {
ignores: [ ignores: [
'.next/', '.next/',
'coverage/',
'node_modules/',
'playwright-report/',
'test-results/',
'next-env.d.ts',
'src/migrations/', // Payload migrations have required but unused params 'src/migrations/', // Payload migrations have required but unused params
'src/migrations_backup/', 'src/migrations_backup/',
], ],

View file

@ -10,7 +10,8 @@
"devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev", "devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev",
"generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap", "generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap",
"generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types", "generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types",
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint", "lint": "cross-env NODE_OPTIONS=--no-deprecation eslint src",
"check:frameworks": "bash ./scripts/check-framework-updates.sh",
"typecheck": "cross-env NODE_OPTIONS=--no-deprecation tsc --noEmit", "typecheck": "cross-env NODE_OPTIONS=--no-deprecation tsc --noEmit",
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx}\" --ignore-unknown", "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx}\" --ignore-unknown",
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx}\" --ignore-unknown", "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx}\" --ignore-unknown",
@ -26,50 +27,50 @@
"prepare": "test -d .git && (ln -sf ../../scripts/detect-secrets.sh .git/hooks/pre-commit 2>/dev/null || true) || true" "prepare": "test -d .git && (ln -sf ../../scripts/detect-secrets.sh .git/hooks/pre-commit 2>/dev/null || true) || true"
}, },
"dependencies": { "dependencies": {
"@payloadcms/db-postgres": "3.65.0", "@payloadcms/db-postgres": "3.68.4",
"@payloadcms/next": "3.65.0", "@payloadcms/next": "3.68.4",
"@payloadcms/plugin-form-builder": "3.65.0", "@payloadcms/plugin-form-builder": "3.68.4",
"@payloadcms/plugin-multi-tenant": "^3.65.0", "@payloadcms/plugin-multi-tenant": "3.68.4",
"@payloadcms/plugin-nested-docs": "3.65.0", "@payloadcms/plugin-nested-docs": "3.68.4",
"@payloadcms/plugin-redirects": "3.65.0", "@payloadcms/plugin-redirects": "3.68.4",
"@payloadcms/plugin-seo": "3.65.0", "@payloadcms/plugin-seo": "3.68.4",
"@payloadcms/richtext-lexical": "3.65.0", "@payloadcms/richtext-lexical": "3.68.4",
"@payloadcms/translations": "^3.65.0", "@payloadcms/translations": "3.68.4",
"@payloadcms/ui": "3.65.0", "@payloadcms/ui": "3.68.4",
"bullmq": "^5.65.1", "bullmq": "^5.65.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "16.4.7", "dotenv": "16.4.7",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"ioredis": "^5.8.2", "ioredis": "^5.8.2",
"next": "15.4.8", "next": "15.5.9",
"node-cron": "^4.2.1", "node-cron": "^4.2.1",
"nodemailer": "^7.0.11", "nodemailer": "^7.0.11",
"payload": "3.65.0", "payload": "3.68.4",
"payload-oapi": "^0.2.5", "payload-oapi": "^0.2.5",
"react": "19.2.1", "react": "19.2.3",
"react-dom": "19.2.1", "react-dom": "19.2.3",
"sharp": "0.34.2" "sharp": "0.34.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.3",
"@playwright/test": "1.56.1", "@playwright/test": "1.57.0",
"@testing-library/react": "16.3.0", "@testing-library/react": "16.3.0",
"@types/node": "^22.5.4", "@types/node": "^22.10.2",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"@types/nodemailer": "^7.0.4", "@types/nodemailer": "^7.0.4",
"@types/react": "19.1.8", "@types/react": "19.2.7",
"@types/react-dom": "19.1.6", "@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "4.5.2", "@vitejs/plugin-react": "4.5.2",
"@vitest/coverage-v8": "^3.2.4", "@vitest/coverage-v8": "4.0.15",
"eslint": "^9.16.0", "eslint": "^9.39.2",
"eslint-config-next": "15.4.7", "eslint-config-next": "15.5.9",
"jsdom": "26.1.0", "jsdom": "26.1.0",
"playwright": "1.56.1", "playwright": "1.57.0",
"playwright-core": "1.56.1", "playwright-core": "1.57.0",
"prettier": "^3.2.5", "prettier": "^3.7.4",
"typescript": "5.7.3", "typescript": "5.9.3",
"vite-tsconfig-paths": "5.1.4", "vite-tsconfig-paths": "6.0.0",
"vitest": "3.2.4" "vitest": "4.0.15"
}, },
"engines": { "engines": {
"node": "^18.20.2 || >=20.9.0", "node": "^18.20.2 || >=20.9.0",

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
if ! command -v pnpm >/dev/null 2>&1; then
echo "pnpm is required to run this check." >&2
exit 1
fi
echo "🔍 Checking Payload + Next.js versions (peer compatibility)…"
pnpm outdated next \
payload \
@payloadcms/next \
@payloadcms/db-postgres \
@payloadcms/plugin-form-builder \
@payloadcms/plugin-multi-tenant \
@payloadcms/plugin-nested-docs \
@payloadcms/plugin-redirects \
@payloadcms/plugin-seo \
@payloadcms/richtext-lexical \
@payloadcms/ui
echo
echo " Review Payload release notes: https://github.com/payloadcms/payload/releases"
echo " Review Next.js release notes: https://nextjs.org/blog"

View file

@ -19,19 +19,6 @@ const TIMELINE_RATE_LIMIT = 30
const TIMELINE_TYPES = ['history', 'milestones', 'releases', 'career', 'events', 'process'] as const const TIMELINE_TYPES = ['history', 'milestones', 'releases', 'career', 'events', 'process'] as const
type TimelineType = (typeof TIMELINE_TYPES)[number] type TimelineType = (typeof TIMELINE_TYPES)[number]
// Event category for filtering
const EVENT_CATEGORIES = [
'milestone',
'founding',
'product',
'team',
'award',
'partnership',
'expansion',
'technology',
'other',
] as const
interface TimelineEvent { interface TimelineEvent {
dateType: string dateType: string
year?: number year?: number

View file

@ -28,9 +28,6 @@ const WORKFLOW_TYPES = [
] as const ] as const
type WorkflowType = (typeof WORKFLOW_TYPES)[number] type WorkflowType = (typeof WORKFLOW_TYPES)[number]
// Step types for filtering
const STEP_TYPES = ['task', 'decision', 'milestone', 'approval', 'wait', 'automatic'] as const
// Valid complexity values (must match Workflows.ts select options) // Valid complexity values (must match Workflows.ts select options)
const COMPLEXITY_VALUES = ['simple', 'medium', 'complex', 'very_complex'] as const const COMPLEXITY_VALUES = ['simple', 'medium', 'complex', 'very_complex'] as const
type ComplexityValue = (typeof COMPLEXITY_VALUES)[number] type ComplexityValue = (typeof COMPLEXITY_VALUES)[number]

View file

@ -169,10 +169,10 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
} }
} }
// Logs abrufen - Type assertion für where da email-logs noch nicht in payload-types type FindArgs = Parameters<typeof payload.find>[0]
const result = await (payload.find as Function)({ const result = await payload.find({
collection: 'email-logs', collection: 'email-logs',
where, where: where as FindArgs['where'],
limit, limit,
sort: '-createdAt', sort: '-createdAt',
depth: 1, depth: 1,

View file

@ -97,33 +97,29 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
baseWhere.tenant = { in: tenantFilter } baseWhere.tenant = { in: tenantFilter }
} }
// Statistiken parallel abrufen - Type assertions für email-logs Collection
const countFn = payload.count as Function
const findFn = payload.find as Function
const [totalResult, sentResult, failedResult, pendingResult, recentFailed] = await Promise.all([ const [totalResult, sentResult, failedResult, pendingResult, recentFailed] = await Promise.all([
// Gesamt // Gesamt
countFn({ payload.count({
collection: 'email-logs', collection: 'email-logs',
where: baseWhere, where: baseWhere,
}), }),
// Gesendet // Gesendet
countFn({ payload.count({
collection: 'email-logs', collection: 'email-logs',
where: { ...baseWhere, status: { equals: 'sent' } }, where: { ...baseWhere, status: { equals: 'sent' } },
}), }),
// Fehlgeschlagen // Fehlgeschlagen
countFn({ payload.count({
collection: 'email-logs', collection: 'email-logs',
where: { ...baseWhere, status: { equals: 'failed' } }, where: { ...baseWhere, status: { equals: 'failed' } },
}), }),
// Ausstehend // Ausstehend
countFn({ payload.count({
collection: 'email-logs', collection: 'email-logs',
where: { ...baseWhere, status: { equals: 'pending' } }, where: { ...baseWhere, status: { equals: 'pending' } },
}), }),
// Letzte 5 fehlgeschlagene (für Quick-View) // Letzte 5 fehlgeschlagene (für Quick-View)
findFn({ payload.find({
collection: 'email-logs', collection: 'email-logs',
where: { ...baseWhere, status: { equals: 'failed' } }, where: { ...baseWhere, status: { equals: 'failed' } },
limit: 5, limit: 5,
@ -145,7 +141,7 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
await Promise.all( await Promise.all(
sources.map(async (source) => { sources.map(async (source) => {
const result = await countFn({ const result = await payload.count({
collection: 'email-logs', collection: 'email-logs',
where: { ...baseWhere, source: { equals: source } }, where: { ...baseWhere, source: { equals: source } },
}) })

View file

@ -8,7 +8,7 @@
import { getPayload } from 'payload' import { getPayload } from 'payload'
import config from '@payload-config' import config from '@payload-config'
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { enqueuePdf, getPdfJobStatus, getPdfJobResult, isQueueAvailable } from '@/lib/queue' import { enqueuePdf, getPdfJobStatus, isQueueAvailable } from '@/lib/queue'
import { generatePdfFromHtml, generatePdfFromUrl } from '@/lib/pdf/pdf-service' import { generatePdfFromHtml, generatePdfFromUrl } from '@/lib/pdf/pdf-service'
import { logAccessDenied } from '@/lib/audit/audit-service' import { logAccessDenied } from '@/lib/audit/audit-service'
import { import {

View file

@ -1,11 +1,4 @@
import configPromise from '@payload-config' export const GET = async () => {
import { getPayload } from 'payload'
export const GET = async (request: Request) => {
const payload = await getPayload({
config: configPromise,
})
return Response.json({ return Response.json({
message: 'This is an example of a custom route.', message: 'This is an example of a custom route.',
}) })

View file

@ -420,7 +420,7 @@ export const Bookings: CollectionConfig = {
timestamps: true, timestamps: true,
hooks: { hooks: {
beforeChange: [ beforeChange: [
({ data, req, operation }) => { ({ data, req }) => {
// Auto-set author for new notes // Auto-set author for new notes
if (data?.internalNotes && req.user) { if (data?.internalNotes && req.user) {
data.internalNotes = data.internalNotes.map((note: Record<string, unknown>) => { data.internalNotes = data.internalNotes.map((note: Record<string, unknown>) => {

View file

@ -1,6 +1,6 @@
// src/collections/ConsentLogs.ts // src/collections/ConsentLogs.ts
import type { CollectionConfig } from 'payload' import type { CollectionConfig, PayloadRequest } from 'payload'
import crypto from 'crypto' import crypto from 'crypto'
import { env } from '../lib/envValidation' import { env } from '../lib/envValidation'
import { authenticatedOnly } from '../lib/tenantAccess' import { authenticatedOnly } from '../lib/tenantAccess'
@ -30,24 +30,21 @@ function anonymizeIp(ip: string, tenantId: string): string {
* Extrahiert die Client-IP aus dem Request. * Extrahiert die Client-IP aus dem Request.
* Berücksichtigt Reverse-Proxy-Header. * Berücksichtigt Reverse-Proxy-Header.
*/ */
function extractClientIp(req: any): string { function extractClientIp(req: PayloadRequest): string {
// X-Forwarded-For kann mehrere IPs enthalten (Client, Proxies) // X-Forwarded-For kann mehrere IPs enthalten (Client, Proxies)
const forwarded = req.headers?.['x-forwarded-for'] const forwarded = req.headers?.get?.('x-forwarded-for')
if (typeof forwarded === 'string') { if (typeof forwarded === 'string') {
return forwarded.split(',')[0].trim() return forwarded.split(',')[0].trim()
} }
if (Array.isArray(forwarded) && forwarded.length > 0) {
return String(forwarded[0]).trim()
}
// X-Real-IP (einzelne IP) // X-Real-IP (einzelne IP)
const realIp = req.headers?.['x-real-ip'] const realIp = req.headers?.get?.('x-real-ip')
if (typeof realIp === 'string') { if (typeof realIp === 'string') {
return realIp.trim() return realIp.trim()
} }
// Fallback: Socket Remote Address // Fallback: unknown (PayloadRequest hat keinen direkten IP-Zugriff mehr)
return req.socket?.remoteAddress || req.ip || 'unknown' return 'unknown'
} }
/** /**

View file

@ -13,21 +13,6 @@ import { logEmailFailed } from '../lib/audit/audit-service'
const failedEmailCounter: Map<number, { count: number; lastReset: number }> = new Map() const failedEmailCounter: Map<number, { count: number; lastReset: number }> = new Map()
const RESET_INTERVAL = 60 * 60 * 1000 // 1 Stunde const RESET_INTERVAL = 60 * 60 * 1000 // 1 Stunde
/**
* Gibt die Anzahl der fehlgeschlagenen E-Mails für einen Tenant zurück
*/
function getFailedCount(tenantId: number): number {
const now = Date.now()
const entry = failedEmailCounter.get(tenantId)
if (!entry || now - entry.lastReset > RESET_INTERVAL) {
failedEmailCounter.set(tenantId, { count: 0, lastReset: now })
return 0
}
return entry.count
}
/** /**
* Inkrementiert den Zähler für fehlgeschlagene E-Mails * Inkrementiert den Zähler für fehlgeschlagene E-Mails
*/ */

View file

@ -1,10 +1,6 @@
// src/hooks/formSubmissionHooks.ts // src/hooks/formSubmissionHooks.ts
import type { import type { CollectionBeforeChangeHook } from 'payload'
CollectionBeforeChangeHook,
CollectionAfterReadHook,
FieldHook,
} from 'payload'
interface InternalNote { interface InternalNote {
note: string note: string
@ -12,12 +8,21 @@ interface InternalNote {
createdAt?: string createdAt?: string
} }
interface ResponseTracking {
responded?: boolean
respondedAt?: string
respondedBy?: number | string | { id: number | string }
method?: string
summary?: string
}
interface FormSubmissionDoc { interface FormSubmissionDoc {
id: number | string id: number | string
status?: string status?: string
readAt?: string readAt?: string
readBy?: number | string | { id: number | string } readBy?: number | string | { id: number | string }
internalNotes?: InternalNote[] internalNotes?: InternalNote[]
responseTracking?: ResponseTracking
[key: string]: unknown [key: string]: unknown
} }
@ -98,7 +103,7 @@ export const setResponseTimestamp: CollectionBeforeChangeHook<FormSubmissionDoc>
return { return {
...data, ...data,
responseTracking: { responseTracking: {
...data.responseTracking, ...(data.responseTracking || {}),
respondedAt: new Date().toISOString(), respondedAt: new Date().toISOString(),
respondedBy: req.user.id, respondedBy: req.user.id,
}, },

View file

@ -154,8 +154,8 @@ export async function createAuditLog(
const maskedNewValue = input.newValue ? maskObject(input.newValue) : undefined const maskedNewValue = input.newValue ? maskObject(input.newValue) : undefined
const maskedMetadata = input.metadata ? maskObject(input.metadata) : undefined const maskedMetadata = input.metadata ? maskObject(input.metadata) : undefined
// Type assertion notwendig bis payload-types.ts regeneriert wird type CreateArgs = Parameters<typeof payload.create>[0]
await (payload.create as Function)({ await payload.create({
collection: 'audit-logs', collection: 'audit-logs',
data: { data: {
action: input.action, action: input.action,
@ -174,7 +174,7 @@ export async function createAuditLog(
}, },
// Bypass Access Control für System-Logging // Bypass Access Control für System-Logging
overrideAccess: true, overrideAccess: true,
}) } as CreateArgs)
} catch (error) { } catch (error) {
// Fehler beim Audit-Logging sollten die Hauptoperation nicht blockieren // Fehler beim Audit-Logging sollten die Hauptoperation nicht blockieren
// Auch Fehlermeldungen maskieren // Auch Fehlermeldungen maskieren
@ -473,13 +473,5 @@ function maskSensitiveData(text: string): string {
return maskString(text) return maskString(text)
} }
/**
* Maskiert Objekte für Audit-Logs (previousValue, newValue, metadata)
*/
function maskAuditData(data: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
if (!data) return undefined
return maskObject(data)
}
// Re-export für externe Nutzung // Re-export für externe Nutzung
export { maskError, maskObject, maskString } export { maskError, maskObject, maskString }

View file

@ -79,6 +79,6 @@ export const localeNames: Record<Locale, { native: string; english: string }> =
* Get locale direction (for RTL support in future) * Get locale direction (for RTL support in future)
*/ */
export function getLocaleDirection(locale: Locale): 'ltr' | 'rtl' { export function getLocaleDirection(locale: Locale): 'ltr' | 'rtl' {
// Both German and English are LTR const rtlLocales: Locale[] = []
return 'ltr' return rtlLocales.includes(locale) ? 'rtl' : 'ltr'
} }

View file

@ -96,7 +96,7 @@ export function startEmailWorker(): Worker<EmailJobData, EmailJobResult> {
console.log(`[EmailWorker] Ready (concurrency: ${CONCURRENCY})`) console.log(`[EmailWorker] Ready (concurrency: ${CONCURRENCY})`)
}) })
emailWorker.on('completed', (job, result) => { emailWorker.on('completed', (job) => {
console.log(`[EmailWorker] Job ${job.id} completed in ${Date.now() - job.timestamp}ms`) console.log(`[EmailWorker] Job ${job.id} completed in ${Date.now() - job.timestamp}ms`)
}) })

View file

@ -31,7 +31,6 @@ async function processPdfJob(job: Job<PdfJobData>): Promise<PdfJobResult> {
options = {}, options = {},
tenantId, tenantId,
documentType, documentType,
correlationId,
} = job.data } = job.data
console.log(`[PdfWorker] Processing job ${job.id} for tenant ${tenantId} (source: ${source})`) console.log(`[PdfWorker] Processing job ${job.id} for tenant ${tenantId} (source: ${source})`)

View file

@ -93,6 +93,9 @@ export interface Config {
jobs: Job; jobs: Job;
downloads: Download; downloads: Download;
events: Event; events: Event;
bookings: Booking;
certifications: Certification;
projects: Project;
'cookie-configurations': CookieConfiguration; 'cookie-configurations': CookieConfiguration;
'cookie-inventory': CookieInventory; 'cookie-inventory': CookieInventory;
'consent-logs': ConsentLog; 'consent-logs': ConsentLog;
@ -135,6 +138,9 @@ export interface Config {
jobs: JobsSelect<false> | JobsSelect<true>; jobs: JobsSelect<false> | JobsSelect<true>;
downloads: DownloadsSelect<false> | DownloadsSelect<true>; downloads: DownloadsSelect<false> | DownloadsSelect<true>;
events: EventsSelect<false> | EventsSelect<true>; events: EventsSelect<false> | EventsSelect<true>;
bookings: BookingsSelect<false> | BookingsSelect<true>;
certifications: CertificationsSelect<false> | CertificationsSelect<true>;
projects: ProjectsSelect<false> | ProjectsSelect<true>;
'cookie-configurations': CookieConfigurationsSelect<false> | CookieConfigurationsSelect<true>; 'cookie-configurations': CookieConfigurationsSelect<false> | CookieConfigurationsSelect<true>;
'cookie-inventory': CookieInventorySelect<false> | CookieInventorySelect<true>; 'cookie-inventory': CookieInventorySelect<false> | CookieInventorySelect<true>;
'consent-logs': ConsentLogsSelect<false> | ConsentLogsSelect<true>; 'consent-logs': ConsentLogsSelect<false> | ConsentLogsSelect<true>;
@ -2584,6 +2590,90 @@ export interface Page {
blockName?: string | null; blockName?: string | null;
blockType: 'comparison'; blockType: 'comparison';
} }
| {
title?: string | null;
subtitle?: string | null;
description?: string | null;
comparisons: {
title?: string | null;
beforeImage: number | Media;
afterImage: number | Media;
beforeLabel?: string | null;
afterLabel?: string | null;
description?: string | null;
category?:
| (
| 'wedding'
| 'portrait'
| 'retouch'
| 'colorgrade'
| 'restore'
| 'composing'
| 'architecture'
| 'product'
| 'other'
)
| null;
/**
* Komma-getrennte Tags für Filterung
*/
tags?: string | null;
metadata?: {
client?: string | null;
date?: string | null;
/**
* z.B. Lightroom, Photoshop
*/
tools?: string | null;
duration?: string | null;
};
showMetadata?: boolean | null;
id?: string | null;
}[];
displayStyle?: ('slider' | 'hover' | 'toggle' | 'side-by-side' | 'fade') | null;
sliderOrientation?: ('horizontal' | 'vertical') | null;
/**
* 0 = links/oben, 100 = rechts/unten
*/
sliderStartPosition?: number | null;
layout?: ('single' | 'grid-2' | 'grid-3' | 'carousel' | 'masonry') | null;
aspectRatio?: ('original' | '16-9' | '4-3' | '3-2' | '1-1' | '2-3' | '9-16') | null;
sliderHandle?: {
style?: ('circle' | 'line' | 'arrows' | 'custom') | null;
color?: ('white' | 'black' | 'primary' | 'accent') | null;
size?: ('small' | 'medium' | 'large') | null;
showLine?: boolean | null;
};
showLabels?: boolean | null;
labelPosition?: ('corners' | 'top' | 'bottom' | 'overlay') | null;
labelStyle?: ('badge' | 'text' | 'pill') | null;
showFilter?: boolean | null;
animation?: {
enableAnimation?: boolean | null;
autoPlay?: boolean | null;
autoPlaySpeed?: number | null;
scrollTrigger?: boolean | null;
};
interactivity?: {
enableZoom?: boolean | null;
enableFullscreen?: boolean | null;
enableSwipe?: boolean | null;
enableKeyboard?: boolean | null;
};
cta?: {
showCta?: boolean | null;
ctaText?: string | null;
ctaLink?: string | null;
ctaStyle?: ('primary' | 'secondary' | 'outline' | 'ghost') | null;
};
backgroundColor?: ('transparent' | 'white' | 'light' | 'dark' | 'black') | null;
borderRadius?: ('none' | 'small' | 'medium' | 'large') | null;
shadow?: ('none' | 'small' | 'medium' | 'large') | null;
spacing?: ('none' | 'small' | 'medium' | 'large') | null;
id?: string | null;
blockName?: string | null;
blockType: 'before-after';
}
)[] )[]
| null; | null;
seo?: { seo?: {
@ -4941,6 +5031,429 @@ export interface Workflow {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* Terminbuchungen für Fotoshootings
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "bookings".
*/
export interface Booking {
id: number;
tenant?: (number | null) | Tenant;
customerName: string;
customerEmail: string;
customerPhone?: string | null;
customerCompany?: string | null;
serviceType:
| 'wedding'
| 'portrait'
| 'business'
| 'event'
| 'product'
| 'family'
| 'newborn'
| 'maternity'
| 'realestate'
| 'other';
/**
* Optional: Verknüpfung mit einem definierten Service
*/
service?: (number | null) | Service;
date: string;
/**
* z.B. 14:00 Uhr
*/
time?: string | null;
duration?: ('30' | '60' | '120' | '180' | '240' | '480' | 'custom') | null;
locationType?: ('studio' | 'outdoor' | 'customer' | 'event' | 'tbd') | null;
locationAddress?: string | null;
/**
* Wie viele Personen sollen fotografiert werden?
*/
participants?: number | null;
/**
* Besondere Wünsche, Ideen oder Anmerkungen
*/
message?: string | null;
/**
* Beispielbilder für gewünschten Stil
*/
referenceImages?:
| {
image?: (number | null) | Media;
note?: string | null;
id?: string | null;
}[]
| null;
status: 'pending' | 'review' | 'confirmed' | 'deposit' | 'completed' | 'cancelled' | 'noshow';
priority?: ('high' | 'normal' | 'low') | null;
pricing?: {
/**
* In Euro
*/
estimatedPrice?: number | null;
finalPrice?: number | null;
depositAmount?: number | null;
depositPaid?: boolean | null;
fullyPaid?: boolean | null;
};
/**
* Nur für interne Verwendung
*/
internalNotes?:
| {
note: string;
author?: (number | null) | User;
createdAt?: string | null;
id?: string | null;
}[]
| null;
contactHistory?:
| {
type: 'email_sent' | 'email_received' | 'call' | 'whatsapp' | 'inperson';
summary: string;
date?: string | null;
id?: string | null;
}[]
| null;
assignedTo?: (number | null) | User;
source?: ('website' | 'phone' | 'email' | 'instagram' | 'facebook' | 'referral' | 'returning' | 'other') | null;
/**
* Kunde hat Datenschutzerklärung akzeptiert
*/
gdprConsent?: boolean | null;
updatedAt: string;
createdAt: string;
}
/**
* Zertifizierungen, Akkreditierungen und Qualitätssiegel
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "certifications".
*/
export interface Certification {
id: number;
tenant?: (number | null) | Tenant;
name: string;
/**
* URL-freundlicher Name
*/
slug: string;
description?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
/**
* Für Übersichten und Meta-Beschreibungen
*/
shortDescription?: string | null;
type: 'iso' | 'din' | 'accreditation' | 'seal' | 'membership' | 'award' | 'license' | 'approval' | 'other';
category?:
| (
| 'quality'
| 'care'
| 'medical'
| 'hygiene'
| 'safety'
| 'privacy'
| 'environment'
| 'hr'
| 'it-security'
| 'accessibility'
| 'other'
)
| null;
issuer: {
name: string;
logo?: (number | null) | Media;
website?: string | null;
country?: ('DE' | 'AT' | 'CH' | 'EU' | 'INT') | null;
};
certNumber?: string | null;
issuedDate?: string | null;
validUntil?: string | null;
renewalCycle?: ('yearly' | '2years' | '3years' | '5years' | 'unlimited') | null;
logo?: (number | null) | Media;
/**
* Das offizielle Zertifikatsdokument
*/
certificate?: (number | null) | Media;
gallery?:
| {
document?: (number | null) | Media;
title?: string | null;
id?: string | null;
}[]
| null;
scope?: {
description?: string | null;
/**
* Für welche Standorte gilt die Zertifizierung?
*/
locations?: (number | Location)[] | null;
/**
* Für welche Leistungen gilt die Zertifizierung?
*/
services?: (number | Service)[] | null;
};
requirements?:
| {
requirement: string;
description?: string | null;
id?: string | null;
}[]
| null;
benefits?:
| {
title: string;
description?: string | null;
icon?: ('check' | 'star' | 'shield' | 'heart' | 'lock' | 'search' | 'clock' | 'document') | null;
id?: string | null;
}[]
| null;
/**
* Historie der durchgeführten Audits
*/
audits?:
| {
date: string;
type?: ('initial' | 'surveillance' | 'recertification' | 'special') | null;
result?: ('passed' | 'conditional' | 'failed') | null;
notes?: string | null;
id?: string | null;
}[]
| null;
status: 'active' | 'pending' | 'renewal' | 'suspended' | 'expired' | 'withdrawn';
visibility?: ('public' | 'request' | 'internal') | null;
/**
* Höhere Zahl = höhere Priorität in der Anzeige
*/
priority?: number | null;
showOnHomepage?: boolean | null;
seo?: {
metaTitle?: string | null;
metaDescription?: string | null;
};
updatedAt: string;
createdAt: string;
}
/**
* Projekte, Spiele und kreative Arbeiten
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "projects".
*/
export interface Project {
id: number;
tenant?: (number | null) | Tenant;
title: string;
slug: string;
/**
* Kurze, prägnante Beschreibung (1 Zeile)
*/
tagline?: string | null;
description?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
/**
* Für Übersichten und Social Media
*/
shortDescription?: string | null;
type: 'game' | 'demo' | 'mod' | 'tool' | 'assets' | 'prototype' | 'gamejam' | 'tutorial' | 'opensource' | 'other';
genres?:
| (
| 'action'
| 'adventure'
| 'rpg'
| 'strategy'
| 'simulation'
| 'puzzle'
| 'horror'
| 'shooter'
| 'platformer'
| 'racing'
| 'sports'
| 'fighting'
| 'music'
| 'visualnovel'
| 'survival'
| 'sandbox'
| 'towerdefense'
| 'roguelike'
| 'indie'
)[]
| null;
platforms?:
| (
| 'windows'
| 'macos'
| 'linux'
| 'web'
| 'ios'
| 'android'
| 'playstation'
| 'xbox'
| 'switch'
| 'steamdeck'
| 'vr'
)[]
| null;
featuredImage: number | Media;
logo?: (number | null) | Media;
screenshots?:
| {
image: number | Media;
caption?: string | null;
id?: string | null;
}[]
| null;
videos?:
| {
type: 'trailer' | 'gameplay' | 'devlog' | 'tutorial' | 'other';
title?: string | null;
/**
* YouTube, Vimeo oder direkter Link
*/
url: string;
thumbnail?: (number | null) | Media;
id?: string | null;
}[]
| null;
techStack?: {
engine?:
| (
| 'unity'
| 'unreal'
| 'godot'
| 'gamemaker'
| 'rpgmaker'
| 'construct'
| 'custom'
| 'renpy'
| 'phaser'
| 'other'
)
| null;
languages?:
| ('csharp' | 'cpp' | 'gdscript' | 'javascript' | 'typescript' | 'python' | 'lua' | 'rust' | 'blueprint')[]
| null;
/**
* z.B. Blender, Aseprite, FMOD
*/
tools?: string | null;
};
requirements?: {
minimum?: {
os?: string | null;
cpu?: string | null;
ram?: string | null;
gpu?: string | null;
storage?: string | null;
};
recommended?: {
os?: string | null;
cpu?: string | null;
ram?: string | null;
gpu?: string | null;
storage?: string | null;
};
};
releaseDate?: string | null;
links?: {
website?: string | null;
steam?: string | null;
itchio?: string | null;
epicGames?: string | null;
gog?: string | null;
playStore?: string | null;
appStore?: string | null;
github?: string | null;
discord?: string | null;
twitter?: string | null;
};
downloads?:
| {
/**
* z.B. "Windows Build", "Demo v0.5"
*/
title: string;
platform?: ('windows' | 'macos' | 'linux' | 'universal') | null;
version?: string | null;
file?: (number | null) | Media;
externalUrl?: string | null;
size?: string | null;
id?: string | null;
}[]
| null;
features?:
| {
title: string;
description?: string | null;
icon?: (number | null) | Media;
id?: string | null;
}[]
| null;
team?:
| {
name: string;
role?: string | null;
link?: string | null;
avatar?: (number | null) | Media;
id?: string | null;
}[]
| null;
gameJam?: {
jamName?: string | null;
theme?: string | null;
/**
* z.B. "48 Stunden"
*/
duration?: string | null;
ranking?: string | null;
jamLink?: string | null;
};
/**
* Verknüpfte Blog-Posts über dieses Projekt
*/
devlogs?: (number | Post)[] | null;
status: 'development' | 'earlyaccess' | 'released' | 'paused' | 'cancelled' | 'completed';
visibility?: ('public' | 'draft' | 'unlisted' | 'private') | null;
featured?: boolean | null;
/**
* Höher = weiter oben
*/
sortOrder?: number | null;
seo?: {
metaTitle?: string | null;
metaDescription?: string | null;
ogImage?: (number | null) | Media;
};
updatedAt: string;
createdAt: string;
}
/** /**
* Cookie-Banner Konfiguration pro Tenant * Cookie-Banner Konfiguration pro Tenant
* *
@ -5469,6 +5982,18 @@ export interface PayloadLockedDocument {
relationTo: 'events'; relationTo: 'events';
value: number | Event; value: number | Event;
} | null) } | null)
| ({
relationTo: 'bookings';
value: number | Booking;
} | null)
| ({
relationTo: 'certifications';
value: number | Certification;
} | null)
| ({
relationTo: 'projects';
value: number | Project;
} | null)
| ({ | ({
relationTo: 'cookie-configurations'; relationTo: 'cookie-configurations';
value: number | CookieConfiguration; value: number | CookieConfiguration;
@ -7442,6 +7967,82 @@ export interface PagesSelect<T extends boolean = true> {
id?: T; id?: T;
blockName?: T; blockName?: T;
}; };
'before-after'?:
| T
| {
title?: T;
subtitle?: T;
description?: T;
comparisons?:
| T
| {
title?: T;
beforeImage?: T;
afterImage?: T;
beforeLabel?: T;
afterLabel?: T;
description?: T;
category?: T;
tags?: T;
metadata?:
| T
| {
client?: T;
date?: T;
tools?: T;
duration?: T;
};
showMetadata?: T;
id?: T;
};
displayStyle?: T;
sliderOrientation?: T;
sliderStartPosition?: T;
layout?: T;
aspectRatio?: T;
sliderHandle?:
| T
| {
style?: T;
color?: T;
size?: T;
showLine?: T;
};
showLabels?: T;
labelPosition?: T;
labelStyle?: T;
showFilter?: T;
animation?:
| T
| {
enableAnimation?: T;
autoPlay?: T;
autoPlaySpeed?: T;
scrollTrigger?: T;
};
interactivity?:
| T
| {
enableZoom?: T;
enableFullscreen?: T;
enableSwipe?: T;
enableKeyboard?: T;
};
cta?:
| T
| {
showCta?: T;
ctaText?: T;
ctaLink?: T;
ctaStyle?: T;
};
backgroundColor?: T;
borderRadius?: T;
shadow?: T;
spacing?: T;
id?: T;
blockName?: T;
};
}; };
seo?: seo?:
| T | T
@ -8490,6 +9091,270 @@ export interface EventsSelect<T extends boolean = true> {
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "bookings_select".
*/
export interface BookingsSelect<T extends boolean = true> {
tenant?: T;
customerName?: T;
customerEmail?: T;
customerPhone?: T;
customerCompany?: T;
serviceType?: T;
service?: T;
date?: T;
time?: T;
duration?: T;
locationType?: T;
locationAddress?: T;
participants?: T;
message?: T;
referenceImages?:
| T
| {
image?: T;
note?: T;
id?: T;
};
status?: T;
priority?: T;
pricing?:
| T
| {
estimatedPrice?: T;
finalPrice?: T;
depositAmount?: T;
depositPaid?: T;
fullyPaid?: T;
};
internalNotes?:
| T
| {
note?: T;
author?: T;
createdAt?: T;
id?: T;
};
contactHistory?:
| T
| {
type?: T;
summary?: T;
date?: T;
id?: T;
};
assignedTo?: T;
source?: T;
gdprConsent?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "certifications_select".
*/
export interface CertificationsSelect<T extends boolean = true> {
tenant?: T;
name?: T;
slug?: T;
description?: T;
shortDescription?: T;
type?: T;
category?: T;
issuer?:
| T
| {
name?: T;
logo?: T;
website?: T;
country?: T;
};
certNumber?: T;
issuedDate?: T;
validUntil?: T;
renewalCycle?: T;
logo?: T;
certificate?: T;
gallery?:
| T
| {
document?: T;
title?: T;
id?: T;
};
scope?:
| T
| {
description?: T;
locations?: T;
services?: T;
};
requirements?:
| T
| {
requirement?: T;
description?: T;
id?: T;
};
benefits?:
| T
| {
title?: T;
description?: T;
icon?: T;
id?: T;
};
audits?:
| T
| {
date?: T;
type?: T;
result?: T;
notes?: T;
id?: T;
};
status?: T;
visibility?: T;
priority?: T;
showOnHomepage?: T;
seo?:
| T
| {
metaTitle?: T;
metaDescription?: T;
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "projects_select".
*/
export interface ProjectsSelect<T extends boolean = true> {
tenant?: T;
title?: T;
slug?: T;
tagline?: T;
description?: T;
shortDescription?: T;
type?: T;
genres?: T;
platforms?: T;
featuredImage?: T;
logo?: T;
screenshots?:
| T
| {
image?: T;
caption?: T;
id?: T;
};
videos?:
| T
| {
type?: T;
title?: T;
url?: T;
thumbnail?: T;
id?: T;
};
techStack?:
| T
| {
engine?: T;
languages?: T;
tools?: T;
};
requirements?:
| T
| {
minimum?:
| T
| {
os?: T;
cpu?: T;
ram?: T;
gpu?: T;
storage?: T;
};
recommended?:
| T
| {
os?: T;
cpu?: T;
ram?: T;
gpu?: T;
storage?: T;
};
};
releaseDate?: T;
links?:
| T
| {
website?: T;
steam?: T;
itchio?: T;
epicGames?: T;
gog?: T;
playStore?: T;
appStore?: T;
github?: T;
discord?: T;
twitter?: T;
};
downloads?:
| T
| {
title?: T;
platform?: T;
version?: T;
file?: T;
externalUrl?: T;
size?: T;
id?: T;
};
features?:
| T
| {
title?: T;
description?: T;
icon?: T;
id?: T;
};
team?:
| T
| {
name?: T;
role?: T;
link?: T;
avatar?: T;
id?: T;
};
gameJam?:
| T
| {
jamName?: T;
theme?: T;
duration?: T;
ranking?: T;
jamLink?: T;
};
devlogs?: T;
status?: T;
visibility?: T;
featured?: T;
sortOrder?: T;
seo?:
| T
| {
metaTitle?: T;
metaDescription?: T;
ogImage?: T;
};
updatedAt?: T;
createdAt?: T;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "cookie-configurations_select". * via the `definition` "cookie-configurations_select".

View file

@ -209,8 +209,8 @@ export default buildConfig({
pool: { pool: {
connectionString: env.DATABASE_URI, connectionString: env.DATABASE_URI,
}, },
// Temporär aktiviert für Events Collection // push: false - Schema-Änderungen nur via Migrationen
push: true, push: false,
}), }),
// Sharp für Bildoptimierung // Sharp für Bildoptimierung
sharp, sharp,