fix: resolve global typecheck errors

This commit is contained in:
Martin Porwoll 2026-02-17 08:57:32 +00:00
parent 6b4dae8eeb
commit 4386ac5d8d
41 changed files with 797 additions and 223 deletions

View file

@ -133,7 +133,7 @@ async function seedRules() {
await payload.create({
collection: 'community-rules',
data: rule,
data: rule as any,
})
console.log(`✅ Created: ${rule.name}`)
created++

View file

@ -53,26 +53,49 @@ async function seed() {
// ============================================
console.log('\n--- Updating Site Settings ---')
await payload.updateGlobal({
slug: 'site-settings',
data: {
siteName: 'porwoll.de',
siteTagline: 'Die Webseite von Martin Porwoll',
contact: {
email: 'info@porwoll.de',
phone: '0800 80 44 100',
address: 'Hans-Böckler-Str. 19\n46236 Bottrop',
},
footer: {
copyrightText: 'Martin Porwoll',
showSocialLinks: true,
},
seo: {
defaultMetaTitle: 'porwoll.de | Die Webseite von Martin Porwoll',
defaultMetaDescription: 'Martin Porwoll - Whistleblower, Unternehmer, Mensch. Engagiert für Patientenwohl und Transparenz im Gesundheitswesen.',
},
const siteSettingsData = {
tenant: tenantId,
siteName: 'porwoll.de',
siteTagline: 'Die Webseite von Martin Porwoll',
contact: {
email: 'info@porwoll.de',
phone: '0800 80 44 100',
},
address: {
street: 'Hans-Böckler-Str. 19',
zip: '46236',
city: 'Bottrop',
country: 'Deutschland',
},
footer: {
copyrightText: 'Martin Porwoll',
showSocialLinks: true,
},
seo: {
defaultMetaTitle: 'porwoll.de | Die Webseite von Martin Porwoll',
defaultMetaDescription:
'Martin Porwoll - Whistleblower, Unternehmer, Mensch. Engagiert für Patientenwohl und Transparenz im Gesundheitswesen.',
},
}
const existingSiteSettings = await payload.find({
collection: 'site-settings',
where: { tenant: { equals: tenantId } },
limit: 1,
})
if (existingSiteSettings.docs.length > 0) {
await payload.update({
collection: 'site-settings',
id: existingSiteSettings.docs[0].id,
data: siteSettingsData as any,
})
} else {
await payload.create({
collection: 'site-settings',
data: siteSettingsData as any,
})
}
console.log('✓ Site Settings updated')
// ============================================
@ -891,43 +914,61 @@ async function seed() {
pageIds[p.slug] = p.id as number
}
await payload.updateGlobal({
slug: 'navigation',
data: {
mainMenu: [
{
label: 'Whistleblowing',
type: 'submenu',
submenu: [
{ label: 'Zytoskandal', linkType: 'page', page: pageIds['zytoskandal'] },
{ label: 'Whistleblowing', linkType: 'page', page: pageIds['whistleblowing'] },
],
},
{
label: 'Unternehmer',
type: 'submenu',
submenu: [
{ label: 'gunshin Holding UG', linkType: 'page', page: pageIds['gunshin-holding'] },
{ label: 'complex care solutions GmbH', linkType: 'page', page: pageIds['complex-care-solutions'] },
],
},
{
label: 'Mensch',
type: 'page',
page: pageIds['mensch'],
},
{
label: 'Kontakt',
type: 'page',
page: pageIds['kontakt'],
},
],
footerMenu: [
{ label: 'Impressum', linkType: 'page', page: pageIds['impressum'] },
{ label: 'Datenschutzerklärung', linkType: 'page', page: pageIds['datenschutz'] },
],
},
const navigationData = {
tenant: tenantId,
title: 'Hauptnavigation',
mainMenu: [
{
label: 'Whistleblowing',
type: 'submenu',
submenu: [
{ label: 'Zytoskandal', linkType: 'page', page: pageIds['zytoskandal'] },
{ label: 'Whistleblowing', linkType: 'page', page: pageIds['whistleblowing'] },
],
},
{
label: 'Unternehmer',
type: 'submenu',
submenu: [
{ label: 'gunshin Holding UG', linkType: 'page', page: pageIds['gunshin-holding'] },
{ label: 'complex care solutions GmbH', linkType: 'page', page: pageIds['complex-care-solutions'] },
],
},
{
label: 'Mensch',
type: 'page',
page: pageIds['mensch'],
},
{
label: 'Kontakt',
type: 'page',
page: pageIds['kontakt'],
},
],
footerMenu: [
{ label: 'Impressum', linkType: 'page', page: pageIds['impressum'] },
{ label: 'Datenschutzerklärung', linkType: 'page', page: pageIds['datenschutz'] },
],
}
const existingNavigation = await payload.find({
collection: 'navigations',
where: { tenant: { equals: tenantId } },
limit: 1,
})
if (existingNavigation.docs.length > 0) {
await payload.update({
collection: 'navigations',
id: existingNavigation.docs[0].id,
data: navigationData as any,
})
} else {
await payload.create({
collection: 'navigations',
data: navigationData as any,
})
}
console.log('✓ Navigation configured')
console.log('\n========================================')

View file

@ -411,35 +411,53 @@ async function seed() {
console.log('Page IDs:', pageIds)
await payload.updateGlobal({
slug: 'navigation',
data: {
mainMenu: [
{
label: 'Whistleblowing',
type: 'submenu',
submenu: [
{ label: 'Zytoskandal', linkType: 'page', page: pageIds['zytoskandal'] },
{ label: 'Whistleblowing', linkType: 'page', page: pageIds['whistleblowing'] },
],
},
{
label: 'Unternehmer',
type: 'submenu',
submenu: [
{ label: 'gunshin Holding UG', linkType: 'page', page: pageIds['gunshin-holding'] },
{ label: 'complex care solutions GmbH', linkType: 'page', page: pageIds['complex-care-solutions'] },
],
},
{ label: 'Mensch', type: 'page', page: pageIds['mensch'] },
{ label: 'Kontakt', type: 'page', page: pageIds['kontakt'] },
],
footerMenu: [
{ label: 'Impressum', linkType: 'page', page: pageIds['impressum'] },
{ label: 'Datenschutzerklärung', linkType: 'page', page: pageIds['datenschutz'] },
],
},
const navigationData = {
tenant: tenantId,
title: 'Hauptnavigation',
mainMenu: [
{
label: 'Whistleblowing',
type: 'submenu',
submenu: [
{ label: 'Zytoskandal', linkType: 'page', page: pageIds['zytoskandal'] },
{ label: 'Whistleblowing', linkType: 'page', page: pageIds['whistleblowing'] },
],
},
{
label: 'Unternehmer',
type: 'submenu',
submenu: [
{ label: 'gunshin Holding UG', linkType: 'page', page: pageIds['gunshin-holding'] },
{ label: 'complex care solutions GmbH', linkType: 'page', page: pageIds['complex-care-solutions'] },
],
},
{ label: 'Mensch', type: 'page', page: pageIds['mensch'] },
{ label: 'Kontakt', type: 'page', page: pageIds['kontakt'] },
],
footerMenu: [
{ label: 'Impressum', linkType: 'page', page: pageIds['impressum'] },
{ label: 'Datenschutzerklärung', linkType: 'page', page: pageIds['datenschutz'] },
],
}
const existingNavigation = await payload.find({
collection: 'navigations',
where: { tenant: { equals: tenantId } },
limit: 1,
})
if (existingNavigation.docs.length > 0) {
await payload.update({
collection: 'navigations',
id: existingNavigation.docs[0].id,
data: navigationData as any,
})
} else {
await payload.create({
collection: 'navigations',
data: navigationData as any,
})
}
console.log('✓ Navigation configured')
console.log('\n========================================')

View file

@ -104,6 +104,10 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
},
})
if (!result.user) {
throw new Error('Login returned no user')
}
// Erfolgreicher Login - afterLogin Hook hat bereits geloggt
// Setze Cookie für die Session
const response = NextResponse.json({

View file

@ -64,8 +64,11 @@ export async function GET(req: NextRequest) {
}
// Prüfen ob die Plattform Facebook oder Instagram ist
const platform = account.platform as string
if (!['facebook', 'instagram'].includes(platform)) {
const platform =
typeof account.platform === 'object' && account.platform
? account.platform.slug
: undefined
if (!platform || !['facebook', 'instagram'].includes(platform)) {
return NextResponse.json(
{ error: 'This endpoint is only for Facebook and Instagram accounts' },
{ status: 400 }

View file

@ -112,7 +112,7 @@ export async function GET(request: NextRequest) {
const topicCounts: Record<string, number> = {}
channelInteractions.forEach((i) => {
if (i.analysis?.topics && Array.isArray(i.analysis.topics)) {
i.analysis.topics.forEach((t: { topic?: string }) => {
i.analysis.topics.forEach((t: { topic?: string | null }) => {
if (t.topic) {
topicCounts[t.topic] = (topicCounts[t.topic] || 0) + 1
}

View file

@ -70,14 +70,14 @@ export async function GET(request: NextRequest) {
}
// Build where clause
const baseWhere: Record<string, unknown> = {
const baseWhere: any = {
publishedAt: {
greater_than_equal: periodStart.toISOString(),
},
}
if (channelId !== 'all') {
baseWhere.socialAccount = { equals: parseInt(channelId) }
baseWhere.socialAccount = { equals: parseInt(channelId, 10) }
}
// Fetch current period data
@ -141,7 +141,7 @@ export async function GET(request: NextRequest) {
const escalations = docs.filter((i) => i.flags?.requiresEscalation).length
// Fetch previous period for comparison
const previousWhere: Record<string, unknown> = {
const previousWhere: any = {
publishedAt: {
greater_than_equal: previousPeriodStart.toISOString(),
less_than: previousPeriodEnd.toISOString(),
@ -149,7 +149,7 @@ export async function GET(request: NextRequest) {
}
if (channelId !== 'all') {
previousWhere.socialAccount = { equals: parseInt(channelId) }
previousWhere.socialAccount = { equals: parseInt(channelId, 10) }
}
const previousInteractions = await payload.find({

View file

@ -68,12 +68,12 @@ export async function GET(request: NextRequest) {
const prevPeriodEnd = periodStart
// Build where clause
const baseWhere: Record<string, unknown> = {
const baseWhere: any = {
publishedAt: { greater_than_equal: periodStart.toISOString() },
}
if (channelId !== 'all') {
baseWhere.socialAccount = { equals: parseInt(channelId) }
baseWhere.socialAccount = { equals: parseInt(channelId, 10) }
}
// Fetch current period data
@ -100,7 +100,7 @@ export async function GET(request: NextRequest) {
const p90Time = percentile(responseTimes, 90)
// Fetch previous period for trend
const prevWhere: Record<string, unknown> = {
const prevWhere: any = {
publishedAt: {
greater_than_equal: prevPeriodStart.toISOString(),
less_than: prevPeriodEnd.toISOString(),
@ -108,7 +108,7 @@ export async function GET(request: NextRequest) {
}
if (channelId !== 'all') {
prevWhere.socialAccount = { equals: parseInt(channelId) }
prevWhere.socialAccount = { equals: parseInt(channelId, 10) }
}
const prevInteractions = await payload.find({

View file

@ -58,14 +58,14 @@ export async function GET(request: NextRequest) {
const periodStart = subDays(now, days)
// Build where clause
const baseWhere: Record<string, unknown> = {
const baseWhere: any = {
publishedAt: {
greater_than_equal: periodStart.toISOString(),
},
}
if (channelId !== 'all') {
baseWhere.socialAccount = { equals: parseInt(channelId) }
baseWhere.socialAccount = { equals: parseInt(channelId, 10) }
}
// Fetch interactions

View file

@ -54,13 +54,13 @@ export async function GET(request: NextRequest) {
const periodStart = subDays(now, days)
// Build where clause
const baseWhere: Record<string, unknown> = {
const baseWhere: any = {
publishedAt: { greater_than_equal: periodStart.toISOString() },
linkedContent: { exists: true },
}
if (channelId !== 'all') {
baseWhere.socialAccount = { equals: parseInt(channelId) }
baseWhere.socialAccount = { equals: parseInt(channelId, 10) }
}
// Fetch interactions with linked content
@ -133,7 +133,7 @@ export async function GET(request: NextRequest) {
const topicCounts: Record<string, number> = {}
contentInteractions.forEach((i) => {
if (i.analysis?.topics && Array.isArray(i.analysis.topics)) {
i.analysis.topics.forEach((t: { topic?: string }) => {
i.analysis.topics.forEach((t: { topic?: string | null }) => {
if (t.topic) {
topicCounts[t.topic] = (topicCounts[t.topic] || 0) + 1
}

View file

@ -53,12 +53,12 @@ export async function GET(request: NextRequest) {
const periodStart = subDays(now, days)
// Build where clause
const baseWhere: Record<string, unknown> = {
const baseWhere: any = {
publishedAt: { greater_than_equal: periodStart.toISOString() },
}
if (channelId !== 'all') {
baseWhere.socialAccount = { equals: parseInt(channelId) }
baseWhere.socialAccount = { equals: parseInt(channelId, 10) }
}
// Fetch interactions
@ -89,7 +89,7 @@ export async function GET(request: NextRequest) {
const sentimentScore = i.analysis?.sentimentScore as number | undefined
i.analysis.topics.forEach((t: { topic?: string }) => {
i.analysis.topics.forEach((t: { topic?: string | null }) => {
if (!t.topic) return
const topic = t.topic.toLowerCase().trim()

View file

@ -69,7 +69,7 @@ export async function GET(req: NextRequest) {
}
// Build query
const where: Record<string, unknown> = {}
const where: any = {}
if (dateFrom || dateTo) {
where.publishedAt = {}

View file

@ -7,7 +7,7 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
const payload = await getPayload({ config })
const { user } = await payload.auth({ headers: req.headers })
if (!user || !(user as Record<string, unknown>).isSuperAdmin) {
if (!user || !(user as { isSuperAdmin?: boolean }).isSuperAdmin) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

View file

@ -15,7 +15,7 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
const limit = parseInt(req.nextUrl.searchParams.get('limit') || '20', 10)
const severity = req.nextUrl.searchParams.get('severity')
const where: Record<string, unknown> = {}
const where: any = {}
if (severity) {
where.severity = { equals: severity }
}

View file

@ -19,7 +19,7 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
const from = req.nextUrl.searchParams.get('from')
const to = req.nextUrl.searchParams.get('to')
const conditions: Record<string, unknown>[] = []
const conditions: any[] = []
if (level) {
conditions.push({ level: { equals: level } })
@ -37,7 +37,7 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
conditions.push({ createdAt: { less_than_equal: to } })
}
const where = conditions.length > 0 ? { and: conditions } : {}
const where: any = conditions.length > 0 ? { and: conditions } : {}
const logs = await payload.find({
collection: 'monitoring-logs',

View file

@ -40,15 +40,38 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
return NextResponse.json({
data: {
redis: resolveSettled(redis, { status: 'offline' }),
postgresql: resolveSettled(postgresql, { status: 'offline' }),
pgbouncer: resolveSettled(pgbouncer, { status: 'offline' }),
smtp: resolveSettled(smtp, { status: 'offline' }),
oauth: resolveSettled(oauth, {
metaOAuth: { status: 'error' },
youtubeOAuth: { status: 'error' },
redis: resolveSettled(redis, {
status: 'offline',
memoryUsedMB: 0,
connectedClients: 0,
opsPerSec: 0,
}),
postgresql: resolveSettled(postgresql, {
status: 'offline',
connections: 0,
maxConnections: 0,
latencyMs: -1,
}),
pgbouncer: resolveSettled(pgbouncer, {
status: 'offline',
activeConnections: 0,
waitingClients: 0,
poolSize: 0,
}),
smtp: resolveSettled(smtp, {
status: 'offline',
lastCheck: new Date().toISOString(),
responseTimeMs: -1,
}),
oauth: resolveSettled(oauth, {
metaOAuth: { status: 'error', tokensTotal: 0, tokensExpiringSoon: 0, tokensExpired: 0 },
youtubeOAuth: { status: 'error', tokensTotal: 0, tokensExpiringSoon: 0, tokensExpired: 0 },
}),
cronJobs: resolveSettled(cronJobs, {
communitySync: { lastRun: '', status: 'unknown' },
tokenRefresh: { lastRun: '', status: 'unknown' },
youtubeSync: { lastRun: '', status: 'unknown' },
}),
cronJobs: resolveSettled(cronJobs, {}),
queues: resolveSettled(queues, {}),
},
timestamp: new Date().toISOString(),

View file

@ -32,7 +32,7 @@ export async function GET(request: NextRequest): Promise<Response> {
const payload = await getPayload({ config })
const { user } = await payload.auth({ headers: request.headers })
if (!user || !(user as Record<string, unknown>).isSuperAdmin) {
if (!user || !(user as { isSuperAdmin?: boolean }).isSuperAdmin) {
return new Response('Unauthorized', { status: 401 })
}

View file

@ -18,22 +18,25 @@ import config from '@payload-config'
import { NextRequest, NextResponse } from 'next/server'
// Lazy imports für Security-Module um Initialisierungsfehler zu vermeiden
let securityModules: {
type SecurityModules = {
authLimiter?: { check: (ip: string) => Promise<{ allowed: boolean; remaining: number; resetIn: number }> }
rateLimitHeaders?: (result: { remaining: number; resetIn: number }, max: number) => Record<string, string>
rateLimitHeaders?: (result: unknown, max: number) => Record<string, string>
getClientIpFromRequest?: (req: NextRequest) => string
isIpBlocked?: (ip: string) => boolean
validateCsrf?: (req: NextRequest) => { valid: boolean; reason?: string }
} | null = null
}
async function getSecurityModules() {
if (securityModules) return securityModules
let securityModules: SecurityModules = {}
let securityModulesLoaded = false
async function getSecurityModules(): Promise<SecurityModules> {
if (securityModulesLoaded) return securityModules
try {
const security = await import('@/lib/security')
securityModules = {
authLimiter: security.authLimiter,
rateLimitHeaders: security.rateLimitHeaders,
rateLimitHeaders: security.rateLimitHeaders as SecurityModules['rateLimitHeaders'],
getClientIpFromRequest: security.getClientIpFromRequest,
isIpBlocked: security.isIpBlocked,
validateCsrf: security.validateCsrf,
@ -43,29 +46,46 @@ async function getSecurityModules() {
securityModules = {}
}
securityModulesLoaded = true
return securityModules
}
// Lazy import für Audit-Service
let auditService: {
logLoginFailed?: (payload: unknown, email: string, reason: string, clientInfo: { ipAddress: string; userAgent: string }) => Promise<void>
logRateLimit?: (payload: unknown, endpoint: string, userId?: number, tenantId?: number) => Promise<void>
} | null = null
type AuditService = {
logLoginFailed?: (
payload: unknown,
email: string,
reason: string,
clientInfo: { ipAddress: string; userAgent: string },
) => Promise<void>
logRateLimit?: (
payload: unknown,
endpoint: string,
userId?: number,
tenantId?: number,
) => Promise<void>
}
async function getAuditService() {
if (auditService) return auditService
let auditService: AuditService = {}
let auditServiceLoaded = false
async function getAuditService(): Promise<AuditService> {
if (auditServiceLoaded) return auditService
try {
const audit = await import('@/lib/audit/audit-service')
auditService = {
logLoginFailed: audit.logLoginFailed,
logRateLimit: audit.logRateLimit,
logLoginFailed: audit.logLoginFailed as AuditService['logLoginFailed'],
logRateLimit: audit.logRateLimit as AuditService['logRateLimit'],
}
} catch (err) {
console.warn('[Login] Audit service not available:', err)
auditService = {}
}
auditServiceLoaded = true
return auditService
}

View file

@ -44,7 +44,7 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
return NextResponse.json({ error: 'start and end required' }, { status: 400 })
}
const where: Record<string, unknown> = {
const where: any = {
scheduledPublishDate: {
greater_than_equal: start,
less_than_equal: end,
@ -74,15 +74,13 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
// Build channel color lookup
const channelColorMap = new Map<number, string>()
for (const ch of channels.docs) {
const branding = (ch as Record<string, unknown>).branding as
| { primaryColor?: string }
| undefined
const branding = (ch as unknown as { branding?: { primaryColor?: string } }).branding
channelColorMap.set(ch.id as number, branding?.primaryColor || '#3788d8')
}
// Build CalendarEvent array for conflict detection
const calendarEvents: CalendarEvent[] = videos.docs.map((v) => {
const doc = v as Record<string, unknown>
const doc = v as unknown as Record<string, unknown>
const channel = resolveRelation(doc.channel)
const series = resolveRelation(doc.series)
@ -97,7 +95,7 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
// Get schedule config from first channel
const defaultSchedule = { longformPerWeek: 1, shortsPerWeek: 4 }
const firstChannel = channels.docs[0] as Record<string, unknown> | undefined
const firstChannel = channels.docs[0] as unknown as Record<string, unknown> | undefined
const publishingSchedule = firstChannel?.publishingSchedule as
| { longformPerWeek?: number; shortsPerWeek?: number }
| undefined
@ -111,7 +109,7 @@ export async function GET(req: NextRequest): Promise<NextResponse> {
// Format for FullCalendar
const events = videos.docs.map((v) => {
const doc = v as Record<string, unknown>
const doc = v as unknown as Record<string, unknown>
const channel = resolveRelation(doc.channel)
const series = resolveRelation(doc.series)
@ -177,7 +175,7 @@ export async function PATCH(req: NextRequest): Promise<NextResponse> {
depth: 0,
})
const status = (content as Record<string, unknown>).status as string
const status = (content as unknown as Record<string, unknown>).status as string
if (status === 'published' || status === 'tracked') {
return NextResponse.json(
{ error: 'Veröffentlichte Videos können nicht verschoben werden' },

View file

@ -58,13 +58,15 @@ export async function POST(req: NextRequest) {
try {
const thumbnailUrl = getYouTubeThumbnail(videoId, 'hq')
const filename = `yt-thumb-${videoId}.jpg`
const tenantId = typeof doc.tenant === 'object' ? doc.tenant?.id : doc.tenant
const title =
typeof doc.title === 'string'
? doc.title
: `YouTube Video ${videoId}`
const mediaId = await downloadAndUploadImage(payload, {
url: thumbnailUrl,
filename,
alt: `Thumbnail: ${typeof doc.title === 'string' ? doc.title : doc.title?.de || videoId}`,
tenantId: tenantId || undefined,
alt: `Thumbnail: ${title}`,
})
if (mediaId) {

View file

@ -42,7 +42,7 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
return NextResponse.json({ error: 'Content not found' }, { status: 404 })
}
const doc = content as Record<string, unknown>
const doc = content as unknown as Record<string, unknown>
if (!doc.videoFile) {
return NextResponse.json({ error: 'No video file attached' }, { status: 400 })

View file

@ -115,7 +115,7 @@ export const ReportSchedules: CollectionConfig = {
width: '50%',
description: 'Format: HH:MM (24-Stunden)',
},
validate: (value) => {
validate: (value: string | null | undefined) => {
if (!value) return true
const regex = /^([01]\d|2[0-3]):([0-5]\d)$/
if (!regex.test(value)) {
@ -233,7 +233,6 @@ export const ReportSchedules: CollectionConfig = {
// === Status (Read-only) ===
{
name: 'statusSection',
type: 'collapsible',
label: 'Status & Statistiken',
admin: {

View file

@ -115,7 +115,7 @@ export const YouTubeContent: CollectionConfig = {
},
}
}
return {}
return true
},
admin: {
position: 'sidebar',

View file

@ -118,7 +118,7 @@ interface ApiResponse {
data: PerformanceData | PipelineData | GoalsData | CommunityData
}
const tabConfig: Array<{ key: Tab; label: string; icon: JSX.Element }> = [
const tabConfig: Array<{ key: Tab; label: string; icon: React.ReactNode }> = [
{
key: 'performance',
label: 'Performance',
@ -201,7 +201,7 @@ const formatDate = (dateString: string): string => {
})
}
const renderTrend = (value: number): JSX.Element | null => {
const renderTrend = (value: number): React.ReactNode => {
if (value === 0) return null
const isUp = value > 0
return (

View file

@ -1,8 +1,11 @@
import type { CollectionAfterChangeHook } from 'payload'
import { NotificationService } from '@/lib/jobs/NotificationService'
import type { YoutubeContent } from '@/payload-types'
type YouTubeContentStatus = NonNullable<YoutubeContent['status']>
interface TransitionContext {
currentStatus: string
currentStatus: YouTubeContentStatus
youtubeVideoId?: string | null
hasAllChecklistsComplete: boolean
}
@ -10,7 +13,7 @@ interface TransitionContext {
/**
* Determines the next status based on current state and conditions
*/
export function getNextStatus(context: TransitionContext): string | null {
export function getNextStatus(context: TransitionContext): YouTubeContentStatus | null {
const { currentStatus, youtubeVideoId } = context
// Upload completed → published
@ -25,7 +28,7 @@ export function getNextStatus(context: TransitionContext): string | null {
* Checks if a manual transition should be suggested
*/
export function shouldTransitionStatus(
status: string,
status: YouTubeContentStatus,
context: { hasVideoFile?: boolean },
): boolean {
if (status === 'approved' && context.hasVideoFile) return true
@ -44,7 +47,7 @@ export const autoStatusTransitions: CollectionAfterChangeHook = async ({
if (operation !== 'update') return doc
const nextStatus = getNextStatus({
currentStatus: doc.status,
currentStatus: doc.status as YouTubeContentStatus,
youtubeVideoId: doc.youtube?.videoId,
hasAllChecklistsComplete: false,
})

View file

@ -1,11 +1,15 @@
// src/hooks/youtubeContent/createTasksOnStatusChange.ts
import type { CollectionAfterChangeHook } from 'payload'
import type { YtTask } from '@/payload-types'
type TaskType = NonNullable<YtTask['taskType']>
type AssignRole = 'creator' | 'producer' | 'editor' | 'manager'
interface TaskTemplate {
title: string
type: string
assignRole: string
type: TaskType
assignRole: AssignRole
}
/**

View file

@ -121,13 +121,14 @@ export async function runSyncAllCommentsJob(payload: Payload): Promise<{
)
const syncService = new CommentsSyncService(payload)
const result = await syncService.syncCommentsForAccount(account.id, {
const result = await syncService.syncComments({
socialAccountId: account.id,
maxComments: 100,
analyzeWithAI: true,
})
const created = result.created || 0
const updated = result.updated || 0
const created = result.newComments || 0
const updated = result.updatedComments || 0
totalNew += created
totalUpdated += updated

View file

@ -79,5 +79,5 @@ export const canAccessAssignedInteractions: Access = ({ req }) => {
{ assignedTo: { equals: user.id } },
{ 'response.sentBy': { equals: user.id } },
],
}
} as any
}

View file

@ -279,6 +279,9 @@ export class FacebookSyncService {
// Platform ID holen
const platformId = await this.getPlatformId()
if (platformId === null) {
throw new Error('Facebook platform not configured')
}
// Interaction-Typ bestimmen (comment oder reply)
const interactionType = comment.parent?.id ? 'reply' : 'comment'
@ -299,10 +302,10 @@ export class FacebookSyncService {
}
// Interaction-Daten zusammenstellen
const interactionData: Record<string, unknown> = {
const interactionData = {
platform: platformId,
socialAccount: account.id,
type: interactionType,
type: interactionType as 'comment' | 'reply',
externalId: comment.id,
parentInteraction: parentInteractionId,
author: {
@ -327,7 +330,12 @@ export class FacebookSyncService {
...(comment.attachment && {
attachments: [
{
type: comment.attachment.type === 'photo' ? 'image' : comment.attachment.type,
type:
comment.attachment.type === 'photo'
? 'image'
: comment.attachment.type === 'video'
? 'video'
: 'link' as 'image' | 'video' | 'link',
url: comment.attachment.url || comment.attachment.media?.image?.src,
},
],
@ -352,6 +360,8 @@ export class FacebookSyncService {
}),
// Interne Notizen mit Post-Kontext
internalNotes: `Facebook Post: ${post.permalink_url || post.id}`,
status: 'new' as const,
priority: 'normal' as const,
}
let interactionId: number
@ -359,6 +369,7 @@ export class FacebookSyncService {
if (isNew) {
const created = await this.payload.create({
collection: 'community-interactions',
draft: false,
data: interactionData,
})
interactionId = created.id as number

View file

@ -324,6 +324,9 @@ export class InstagramSyncService {
// Platform ID holen
const platformId = await this.getPlatformId()
if (platformId === null) {
throw new Error('Instagram platform not configured')
}
// Interaction-Typ bestimmen
const interactionType = parentCommentId ? 'reply' : 'comment'
@ -344,10 +347,10 @@ export class InstagramSyncService {
}
// Interaction-Daten zusammenstellen
const interactionData: Record<string, unknown> = {
const interactionData = {
platform: platformId,
socialAccount: account.id,
type: interactionType,
type: interactionType as 'comment' | 'reply',
externalId: comment.id,
parentInteraction: parentInteractionId,
author: {
@ -386,6 +389,8 @@ export class InstagramSyncService {
}),
// Interne Notizen mit Media-Kontext
internalNotes: `Instagram Post: ${media.permalink}`,
status: 'new' as const,
priority: 'normal' as const,
}
let interactionId: number
@ -393,6 +398,7 @@ export class InstagramSyncService {
if (isNew) {
const created = await this.payload.create({
collection: 'community-interactions',
draft: false,
data: interactionData,
})
interactionId = created.id as number
@ -494,12 +500,15 @@ export class InstagramSyncService {
// Platform ID holen
const platformId = await this.getPlatformId()
if (platformId === null) {
throw new Error('Instagram platform not configured')
}
// Mention als Interaction speichern
const interactionData: Record<string, unknown> = {
const interactionData = {
platform: platformId,
socialAccount: account.id,
type: 'mention',
type: 'mention' as const,
externalId: `mention_${mention.id}`,
author: {
name: mention.username,
@ -528,10 +537,13 @@ export class InstagramSyncService {
},
}),
internalNotes: `Instagram Mention: ${mention.permalink}`,
status: 'new' as const,
priority: 'normal' as const,
}
await this.payload.create({
collection: 'community-interactions',
draft: false,
data: interactionData,
})

View file

@ -146,7 +146,7 @@ export class MetaBaseClient {
const nextResponse = await fetch(nextUrl)
const nextData: MetaPaginatedResponse<T> = await nextResponse.json()
if (nextData.error) {
if ('error' in (nextData as unknown as MetaErrorResponse)) {
throw new MetaApiError((nextData as unknown as MetaErrorResponse).error)
}

View file

@ -231,6 +231,8 @@ export class CommentsSyncService {
isFromInfluencer: false,
},
}),
status: 'new' as const,
priority: 'normal' as const,
}
let parentInteractionId: number | null = null
@ -238,6 +240,7 @@ export class CommentsSyncService {
if (isNew) {
const created = await this.payload.create({
collection: 'community-interactions',
draft: false,
data: interactionData,
})
parentInteractionId = created.id
@ -314,6 +317,7 @@ export class CommentsSyncService {
await this.payload.create({
collection: 'community-interactions',
draft: false,
data: {
platform: platformId,
socialAccount: account.id,
@ -356,6 +360,8 @@ export class CommentsSyncService {
isFromInfluencer: false,
},
}),
status: 'new' as const,
priority: 'normal' as const,
},
})

View file

@ -34,7 +34,7 @@ interface CommentThread {
export class YouTubeClient {
private youtube: youtube_v3.Youtube
private oauth2Client: ReturnType<typeof google.auth.OAuth2>
private oauth2Client: InstanceType<typeof google.auth.OAuth2>
private payload: Payload
constructor(credentials: YouTubeCredentials, payload: Payload) {

View file

@ -60,13 +60,13 @@ export function evaluateCondition(
// ============================================================================
interface AlertRule {
id: string
id: number
name: string
metric: string
condition: AlertCondition
threshold: number
severity: AlertSeverity
channels: string[]
channels: Array<'email' | 'slack' | 'discord'>
recipients?: {
emails?: Array<{ email: string }>
slackWebhook?: string
@ -132,9 +132,10 @@ export class AlertEvaluator {
if (value === undefined) continue
if (evaluateCondition(rule.condition, value, rule.threshold)) {
if (this.shouldFire(rule.id, rule.cooldownMinutes)) {
const ruleKey = String(rule.id)
if (this.shouldFire(ruleKey, rule.cooldownMinutes)) {
await this.dispatchAlert(payload, rule, value)
this.recordFired(rule.id)
this.recordFired(ruleKey)
}
}
}

View file

@ -40,9 +40,9 @@ export interface MonitoringLoggerInstance {
}
/** Cached Payload instance — resolved once, reused for all subsequent writes. */
let cachedPayload: { create: (...args: unknown[]) => Promise<unknown> } | null = null
let cachedPayload: any = null
async function getPayloadInstance() {
async function getPayloadInstance(): Promise<any> {
if (cachedPayload) return cachedPayload
const { getPayload } = await import('payload')
const config = (await import(/* @vite-ignore */ '@payload-config')).default

View file

@ -258,7 +258,7 @@ export async function checkOAuthTokens(): Promise<{
const youtube = { tokensTotal: 0, tokensExpiringSoon: 0, tokensExpired: 0 }
for (const account of accounts.docs) {
const doc = account as Record<string, unknown>
const doc = account as unknown as Record<string, unknown>
const target = doc.platform === 'youtube' ? youtube : meta
target.tokensTotal++
@ -304,7 +304,7 @@ export async function checkCronJobs(): Promise<CronStatuses> {
if (logs.docs.length === 0) return unknownStatus
const doc = logs.docs[0] as Record<string, unknown>
const doc = logs.docs[0] as unknown as Record<string, unknown>
return {
lastRun: doc.createdAt as string,
status: doc.level === 'error' ? 'failed' : 'ok',

View file

@ -13,9 +13,9 @@ let interval: ReturnType<typeof setInterval> | null = null
const alertEvaluator = new AlertEvaluator()
/** Cached Payload instance — resolved once, reused on every tick. */
let cachedPayload: { create: (...args: unknown[]) => Promise<unknown>; find: (...args: unknown[]) => Promise<unknown> } | null = null
let cachedPayload: any = null
async function getPayloadInstance() {
async function getPayloadInstance(): Promise<any> {
if (cachedPayload) return cachedPayload
const { getPayload } = await import('payload')
const config = (await import(/* @vite-ignore */ '@payload-config')).default

View file

@ -144,7 +144,7 @@ export class ReportGeneratorService {
depth: 0,
overrideAccess: true,
})
const tenant = (account as Record<string, unknown>)?.tenant
const tenant = (account as unknown as Record<string, unknown>)?.tenant
if (typeof tenant === 'number') return tenant
if (typeof tenant === 'object' && tenant && 'id' in (tenant as Record<string, unknown>)) {
return (tenant as { id: number }).id
@ -198,12 +198,13 @@ export class ReportGeneratorService {
}
// Update schedule stats
const existingSendCount = (await this.getSchedule(schedule.id))?.sendCount ?? 0
await this.payload.update({
collection: 'report-schedules',
id: schedule.id,
data: {
lastSentAt: new Date().toISOString(),
sendCount: (await this.getSchedule(schedule.id))?.sendCount + 1 || 1,
sendCount: existingSendCount + 1,
lastError: null,
},
})
@ -289,10 +290,12 @@ export class ReportGeneratorService {
limit: 1000,
})
const replied = interactions.docs.filter((i) => i.status === 'replied')
const platforms = interactions.docs.reduce(
const docs = interactions.docs as any[]
const replied = docs.filter((i) => i.status === 'replied')
const platforms = docs.reduce(
(acc, i) => {
const platform = (i.platform as string) || 'unknown'
const platform = this.resolvePlatformName(i.platform)
acc[platform] = (acc[platform] || 0) + 1
return acc
},
@ -303,9 +306,9 @@ export class ReportGeneratorService {
let totalResponseTime = 0
let responseCount = 0
for (const interaction of replied) {
if (interaction.repliedAt && interaction.createdAt) {
if (interaction.response?.sentAt && interaction.createdAt) {
const responseTime =
new Date(interaction.repliedAt as string).getTime() -
new Date(interaction.response.sentAt as string).getTime() -
new Date(interaction.createdAt as string).getTime()
totalResponseTime += responseTime
responseCount++
@ -314,10 +317,13 @@ export class ReportGeneratorService {
return {
totalInteractions: interactions.totalDocs,
newInteractions: interactions.docs.filter((i) => i.status === 'new').length,
newInteractions: docs.filter((i) => i.status === 'new').length,
repliedCount: replied.length,
avgResponseTime: responseCount > 0 ? totalResponseTime / responseCount / (1000 * 60 * 60) : 0,
platforms: Object.entries(platforms).map(([platform, count]) => ({ platform, count })),
platforms: Object.entries(platforms).map(([platform, count]) => ({
platform,
count: typeof count === 'number' ? count : 0,
})),
}
}
@ -342,14 +348,16 @@ export class ReportGeneratorService {
limit: 1000,
})
const docs = interactions.docs as any[]
let positive = 0
let neutral = 0
let negative = 0
const dailyData: Record<string, { positive: number; neutral: number; negative: number }> = {}
for (const interaction of interactions.docs) {
const sentiment = (interaction.aiAnalysis as any)?.sentiment || 'neutral'
for (const interaction of docs) {
const sentiment = interaction.analysis?.sentiment || 'neutral'
const date = new Date(interaction.createdAt as string).toISOString().split('T')[0]
if (!dailyData[date]) {
@ -399,24 +407,26 @@ export class ReportGeneratorService {
limit: 1000,
})
const replied = interactions.docs.filter((i) => i.status === 'replied')
const pending = interactions.docs.filter((i) => i.status === 'new' || i.status === 'in_review')
const docs = interactions.docs as any[]
const replied = docs.filter((i) => i.status === 'replied')
const pending = docs.filter((i) => i.status === 'new' || i.status === 'in_review')
// Berechne durchschnittliche Antwortzeit
let totalTime = 0
let count = 0
const platformData: Record<string, { totalTime: number; count: number; replied: number }> = {}
for (const interaction of interactions.docs) {
const platform = (interaction.platform as string) || 'unknown'
for (const interaction of docs) {
const platform = this.resolvePlatformName(interaction.platform)
if (!platformData[platform]) {
platformData[platform] = { totalTime: 0, count: 0, replied: 0 }
}
platformData[platform].count++
if (interaction.status === 'replied' && interaction.repliedAt && interaction.createdAt) {
if (interaction.status === 'replied' && interaction.response?.sentAt && interaction.createdAt) {
const responseTime =
new Date(interaction.repliedAt as string).getTime() -
new Date(interaction.response.sentAt as string).getTime() -
new Date(interaction.createdAt as string).getTime()
totalTime += responseTime
count++
@ -459,14 +469,22 @@ export class ReportGeneratorService {
limit: 1000,
})
const docs = interactions.docs as any[]
// Top Posts nach Kommentaren gruppieren
const postData: Record<string, { title: string; platform: string; comments: number }> = {}
const topicCount: Record<string, number> = {}
for (const interaction of interactions.docs) {
const postId = (interaction.sourceId as string) || 'unknown'
const platform = (interaction.platform as string) || 'unknown'
const title = (interaction.sourceTitle as string) || postId
for (const interaction of docs) {
const linkedContent = interaction.linkedContent
const postId =
typeof linkedContent === 'object' && linkedContent && 'id' in linkedContent
? String((linkedContent as { id: number }).id)
: typeof linkedContent === 'number'
? String(linkedContent)
: 'unknown'
const platform = this.resolvePlatformName(interaction.platform)
const title = this.resolveContentTitle(linkedContent, postId)
if (!postData[postId]) {
postData[postId] = { title, platform, comments: 0 }
@ -474,9 +492,16 @@ export class ReportGeneratorService {
postData[postId].comments++
// Topics aus AI-Analyse sammeln
const topics = (interaction.aiAnalysis as any)?.topics || []
const topics = Array.isArray(interaction.analysis?.topics)
? interaction.analysis.topics
: []
for (const topic of topics) {
topicCount[topic] = (topicCount[topic] || 0) + 1
const topicName =
typeof topic === 'string'
? topic
: (topic as { topic?: string | null })?.topic || ''
if (!topicName) continue
topicCount[topicName] = (topicCount[topicName] || 0) + 1
}
}
@ -502,7 +527,7 @@ export class ReportGeneratorService {
}.pdf`
try {
const result = await generatePdfFromHtml(html, { filename })
const result = await generatePdfFromHtml(html)
if (!result.success || !result.buffer) {
throw new Error(result.error || 'PDF generation failed')
@ -753,6 +778,27 @@ export class ReportGeneratorService {
return html
}
private resolvePlatformName(platform: unknown): string {
if (typeof platform === 'string') return platform
if (typeof platform === 'number') return String(platform)
if (platform && typeof platform === 'object') {
const p = platform as { slug?: string; name?: string }
return p.slug || p.name || 'unknown'
}
return 'unknown'
}
private resolveContentTitle(linkedContent: unknown, fallbackId: string): string {
if (!linkedContent || typeof linkedContent !== 'object') return fallbackId
const content = linkedContent as { title?: unknown }
if (typeof content.title === 'string') return content.title
if (content.title && typeof content.title === 'object') {
const localized = content.title as { de?: string; en?: string }
return localized.de || localized.en || fallbackId
}
return fallbackId
}
/**
* Generiert einfachen E-Mail-Body für Attachments
*/

View file

@ -289,20 +289,21 @@ export class RulesEngine {
if (!userId) return
try {
const relatedVideo =
typeof interaction.linkedContent === 'object'
? interaction.linkedContent.id
: interaction.linkedContent
// Prüfen ob yt-notifications Collection existiert
await this.payload.create({
collection: 'yt-notifications',
data: {
user: userId,
type: 'community_rule_triggered',
recipient: userId,
type: 'system',
title: `🔔 Rule "${rule.name}" triggered`,
message: `New interaction from ${interaction.author?.name || 'Unknown'}: "${(interaction.message || '').substring(0, 100)}..."`,
relatedContent:
typeof interaction.linkedContent === 'object'
? interaction.linkedContent.id
: interaction.linkedContent,
priority: interaction.priority === 'urgent' ? 'urgent' : 'normal',
isRead: false,
relatedVideo,
read: false,
},
})
} catch (error) {

View file

@ -74,7 +74,7 @@ export const canAccessAssignedContent: Access = ({ req }) => {
{ assignedTo: { equals: user.id } },
{ createdBy: { equals: user.id } },
],
}
} as any
}
/**

View file

@ -121,6 +121,10 @@ export interface Config {
'privacy-policy-settings': PrivacyPolicySetting;
'email-logs': EmailLog;
'audit-logs': AuditLog;
'monitoring-snapshots': MonitoringSnapshot;
'monitoring-logs': MonitoringLog;
'monitoring-alert-rules': MonitoringAlertRule;
'monitoring-alert-history': MonitoringAlertHistory;
'site-settings': SiteSetting;
navigations: Navigation;
forms: Form;
@ -187,6 +191,10 @@ export interface Config {
'privacy-policy-settings': PrivacyPolicySettingsSelect<false> | PrivacyPolicySettingsSelect<true>;
'email-logs': EmailLogsSelect<false> | EmailLogsSelect<true>;
'audit-logs': AuditLogsSelect<false> | AuditLogsSelect<true>;
'monitoring-snapshots': MonitoringSnapshotsSelect<false> | MonitoringSnapshotsSelect<true>;
'monitoring-logs': MonitoringLogsSelect<false> | MonitoringLogsSelect<true>;
'monitoring-alert-rules': MonitoringAlertRulesSelect<false> | MonitoringAlertRulesSelect<true>;
'monitoring-alert-history': MonitoringAlertHistorySelect<false> | MonitoringAlertHistorySelect<true>;
'site-settings': SiteSettingsSelect<false> | SiteSettingsSelect<true>;
navigations: NavigationsSelect<false> | NavigationsSelect<true>;
forms: FormsSelect<false> | FormsSelect<true>;
@ -306,6 +314,10 @@ export interface YoutubeChannel {
language: 'de' | 'en';
category: 'lifestyle' | 'corporate' | 'b2b';
status: 'active' | 'planned' | 'paused' | 'archived';
/**
* Profil-Thumbnail URL von YouTube (automatisch befüllt)
*/
channelThumbnailUrl?: string | null;
branding?: {
/**
* z.B. #1278B3
@ -490,7 +502,7 @@ export interface Tenant {
/**
* Hostname ohne Protokoll (z.B. smtp.gmail.com)
*/
host: string;
host?: string | null;
/**
* 587 (STARTTLS) oder 465 (SSL)
*/
@ -502,7 +514,7 @@ export interface Tenant {
/**
* Meist die E-Mail-Adresse
*/
user: string;
user?: string | null;
/**
* Leer lassen um bestehendes Passwort zu behalten
*/
@ -524,11 +536,6 @@ export interface Page {
* URL-Pfad (z.B. "ueber-uns" / "about-us")
*/
slug: string;
hero?: {
image?: (number | null) | Media;
headline?: string | null;
subline?: string | null;
};
layout?:
| (
| {
@ -793,7 +800,13 @@ export interface Page {
headline?: string | null;
cards?:
| {
mediaType?: ('none' | 'image' | 'icon') | null;
image?: (number | null) | Media;
/**
* Lucide Icon-Name (z.B. "heart", "star", "shield-check", "camera")
*/
icon?: string | null;
iconPosition?: ('top' | 'left') | null;
title: string;
description?: string | null;
link?: string | null;
@ -6377,6 +6390,17 @@ export interface YoutubeContent {
subscribersGained?: number | null;
lastSyncedAt?: string | null;
};
/**
* Für ROI-Berechnung (manuell pflegen)
*/
costs?: {
estimatedProductionHours?: number | null;
estimatedProductionCost?: number | null;
/**
* AdSense + Sponsoring + Affiliate
*/
estimatedRevenue?: number | null;
};
/**
* Nur für das Team sichtbar
*/
@ -7418,6 +7442,232 @@ export interface AuditLog {
updatedAt: string;
createdAt: string;
}
/**
* Historische System-Metriken für Trend-Charts
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "monitoring-snapshots".
*/
export interface MonitoringSnapshot {
id: number;
timestamp: string;
/**
* System-Ressourcen (CPU, RAM, Disk)
*/
system?: {
cpuUsagePercent?: number | null;
memoryUsedMB?: number | null;
memoryTotalMB?: number | null;
memoryUsagePercent?: number | null;
diskUsedGB?: number | null;
diskTotalGB?: number | null;
diskUsagePercent?: number | null;
loadAvg1?: number | null;
loadAvg5?: number | null;
/**
* Uptime in Sekunden
*/
uptime?: number | null;
};
/**
* Service-Status (PM2-Prozesse, Datenbank, Cache)
*/
services?: {
payload?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
queueWorker?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
postgresql?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
pgbouncer?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
redis?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
};
/**
* Externe Services (SMTP, OAuth, Cron)
*/
external?: {
smtp?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
metaOAuth?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
youtubeOAuth?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
cronJobs?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
};
/**
* Performance-Metriken
*/
performance?: {
avgResponseTimeMs?: number | null;
p95ResponseTimeMs?: number | null;
p99ResponseTimeMs?: number | null;
errorRate?: number | null;
requestsPerMinute?: number | null;
};
updatedAt: string;
createdAt: string;
}
/**
* Structured Logs für Business-Events
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "monitoring-logs".
*/
export interface MonitoringLog {
id: number;
level: 'debug' | 'info' | 'warn' | 'error' | 'fatal';
source: 'payload' | 'queue-worker' | 'cron' | 'email' | 'oauth' | 'sync';
message: string;
/**
* Strukturierte Metadaten
*/
context?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
/**
* Korrelations-ID
*/
requestId?: string | null;
userId?: (number | null) | User;
tenant?: (number | null) | Tenant;
/**
* Dauer in ms
*/
duration?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* Konfigurierbare Alert-Regeln
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "monitoring-alert-rules".
*/
export interface MonitoringAlertRule {
id: number;
name: string;
/**
* z.B. system.cpuUsagePercent, services.redis.memoryUsedMB
*/
metric: string;
condition: 'gt' | 'lt' | 'eq' | 'gte' | 'lte';
threshold: number;
severity: 'warning' | 'error' | 'critical';
channels: ('email' | 'slack' | 'discord')[];
recipients?: {
emails?:
| {
email: string;
id?: string | null;
}[]
| null;
slackWebhook?: string | null;
discordWebhook?: string | null;
};
/**
* Minimaler Abstand zwischen gleichen Alerts
*/
cooldownMinutes?: number | null;
enabled?: boolean | null;
/**
* Optional: Tenant-spezifische Regel
*/
tenant?: (number | null) | Tenant;
updatedAt: string;
createdAt: string;
}
/**
* Alert-Log (WORM - Write Once)
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "monitoring-alert-history".
*/
export interface MonitoringAlertHistory {
id: number;
rule?: (number | null) | MonitoringAlertRule;
metric: string;
value: number;
threshold: number;
severity: 'warning' | 'error' | 'critical';
message: string;
channelsSent?: ('email' | 'slack' | 'discord')[] | null;
resolvedAt?: string | null;
acknowledgedBy?: (number | null) | User;
updatedAt: string;
createdAt: string;
}
/**
* Allgemeine Website-Einstellungen pro Tenant
*
@ -7800,6 +8050,22 @@ export interface PayloadLockedDocument {
relationTo: 'audit-logs';
value: number | AuditLog;
} | null)
| ({
relationTo: 'monitoring-snapshots';
value: number | MonitoringSnapshot;
} | null)
| ({
relationTo: 'monitoring-logs';
value: number | MonitoringLog;
} | null)
| ({
relationTo: 'monitoring-alert-rules';
value: number | MonitoringAlertRule;
} | null)
| ({
relationTo: 'monitoring-alert-history';
value: number | MonitoringAlertHistory;
} | null)
| ({
relationTo: 'site-settings';
value: number | SiteSetting;
@ -8061,13 +8327,6 @@ export interface PagesSelect<T extends boolean = true> {
tenant?: T;
title?: T;
slug?: T;
hero?:
| T
| {
image?: T;
headline?: T;
subline?: T;
};
layout?:
| T
| {
@ -8293,7 +8552,10 @@ export interface PagesSelect<T extends boolean = true> {
cards?:
| T
| {
mediaType?: T;
image?: T;
icon?: T;
iconPosition?: T;
title?: T;
description?: T;
link?: T;
@ -11460,6 +11722,7 @@ export interface YoutubeChannelsSelect<T extends boolean = true> {
language?: T;
category?: T;
status?: T;
channelThumbnailUrl?: T;
branding?:
| T
| {
@ -11627,6 +11890,13 @@ export interface YoutubeContentSelect<T extends boolean = true> {
subscribersGained?: T;
lastSyncedAt?: T;
};
costs?:
| T
| {
estimatedProductionHours?: T;
estimatedProductionCost?: T;
estimatedRevenue?: T;
};
internalNotes?: T;
updatedAt?: T;
createdAt?: T;
@ -12274,6 +12544,117 @@ export interface AuditLogsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "monitoring-snapshots_select".
*/
export interface MonitoringSnapshotsSelect<T extends boolean = true> {
timestamp?: T;
system?:
| T
| {
cpuUsagePercent?: T;
memoryUsedMB?: T;
memoryTotalMB?: T;
memoryUsagePercent?: T;
diskUsedGB?: T;
diskTotalGB?: T;
diskUsagePercent?: T;
loadAvg1?: T;
loadAvg5?: T;
uptime?: T;
};
services?:
| T
| {
payload?: T;
queueWorker?: T;
postgresql?: T;
pgbouncer?: T;
redis?: T;
};
external?:
| T
| {
smtp?: T;
metaOAuth?: T;
youtubeOAuth?: T;
cronJobs?: T;
};
performance?:
| T
| {
avgResponseTimeMs?: T;
p95ResponseTimeMs?: T;
p99ResponseTimeMs?: T;
errorRate?: T;
requestsPerMinute?: T;
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "monitoring-logs_select".
*/
export interface MonitoringLogsSelect<T extends boolean = true> {
level?: T;
source?: T;
message?: T;
context?: T;
requestId?: T;
userId?: T;
tenant?: T;
duration?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "monitoring-alert-rules_select".
*/
export interface MonitoringAlertRulesSelect<T extends boolean = true> {
name?: T;
metric?: T;
condition?: T;
threshold?: T;
severity?: T;
channels?: T;
recipients?:
| T
| {
emails?:
| T
| {
email?: T;
id?: T;
};
slackWebhook?: T;
discordWebhook?: T;
};
cooldownMinutes?: T;
enabled?: T;
tenant?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "monitoring-alert-history_select".
*/
export interface MonitoringAlertHistorySelect<T extends boolean = true> {
rule?: T;
metric?: T;
value?: T;
threshold?: T;
severity?: T;
message?: T;
channelsSent?: T;
resolvedAt?: T;
acknowledgedBy?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "site-settings_select".