cms.c2sgmbh/prompts/community-phase1.md
Martin Porwoll 77f70876f4 chore: add Claude Code config, prompts, and tenant setup scripts
- Add .claude/ configuration (agents, commands, hooks, get-shit-done workflows)
- Add prompts/ directory with development planning documents
- Add scripts/setup-tenants/ with tenant configuration
- Add docs/screenshots/
- Remove obsolete phase2.2-corrections-report.md
- Update pnpm-lock.yaml
- Update detect-secrets.sh to ignore setup.sh (env var usage, not secrets)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 10:18:05 +00:00

2240 lines
60 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 1: Community Core Detailplan
## Übersicht
**Ziel:** Community Management direkt im Hub mit YouTube API Integration und Vorbereitung für LinkedIn/Instagram/Facebook.
**Voraussetzung:** YouTube Operations Hub muss implementiert sein (YouTubeChannels, YouTubeContent Collections).
**Kern-Features:**
1. Community Inbox (Unified View)
2. YouTube Comments Sync (Direkte API)
3. Response Templates mit Variablen
4. Medical Flag System (CCS-spezifisch)
5. AI-gestützte Sentiment-Analyse (Claude API)
6. Multi-Platform Grundstruktur
---
## Architektur Phase 1
```
┌─────────────────────────────────────────────────────────────────────────┐
│ COMMUNITY CORE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ COMMUNITY INBOX VIEW │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ YouTube │ │ LinkedIn │ │Instagram │ │ Facebook │ │ │
│ │ │ ✓ │ │ (prep) │ │ (prep) │ │ (prep) │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ INTEGRATION LAYER │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │ │
│ │ │ YouTube Data │ │ Claude AI │ │ Notification │ │ │
│ │ │ API v3 │ │ (Sentiment) │ │ Service │ │ │
│ │ └─────────────────┘ └─────────────────┘ └────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ Platform Adapters (Prepared for Phase 2) │ │ │
│ │ │ • YouTubeAdapter ✓ • LinkedInAdapter (stub) │ │ │
│ │ │ • InstagramAdapter (stub) • FacebookAdapter (stub) │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ PAYLOAD COLLECTIONS │ │
│ │ │ │
│ │ • social-platforms • community-interactions │ │
│ │ • social-accounts • community-templates │ │
│ │ • community-rules │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## Teil 1: Access Control erweitern
### 1.0 Community Access Functions
```typescript
// src/lib/communityAccess.ts
import type { Access } from 'payload'
interface UserWithRoles {
id: number
isSuperAdmin?: boolean
is_super_admin?: boolean
youtubeRole?: 'none' | 'viewer' | 'editor' | 'producer' | 'creator' | 'manager'
youtube_role?: 'none' | 'viewer' | 'editor' | 'producer' | 'creator' | 'manager'
communityRole?: 'none' | 'viewer' | 'moderator' | 'manager'
community_role?: 'none' | 'viewer' | 'moderator' | 'manager'
}
const checkIsSuperAdmin = (user: UserWithRoles | null): boolean => {
if (!user) return false
return Boolean(user.isSuperAdmin || user.is_super_admin)
}
const getCommunityRole = (user: UserWithRoles | null): string | undefined => {
if (!user) return undefined
return user.communityRole || user.community_role
}
const getYouTubeRole = (user: UserWithRoles | null): string | undefined => {
if (!user) return undefined
return user.youtubeRole || user.youtube_role
}
/**
* Prüft ob User Community-Manager oder Super-Admin ist
*/
export const isCommunityManager: Access = ({ req }) => {
const user = req.user as UserWithRoles | null
if (!user) return false
if (checkIsSuperAdmin(user)) return true
// YouTube-Manager haben auch Community-Zugriff
if (getYouTubeRole(user) === 'manager') return true
return getCommunityRole(user) === 'manager'
}
/**
* Prüft ob User mindestens Moderator-Rechte hat
*/
export const isCommunityModeratorOrAbove: Access = ({ req }) => {
const user = req.user as UserWithRoles | null
if (!user) return false
if (checkIsSuperAdmin(user)) return true
if (['manager', 'creator'].includes(getYouTubeRole(user) || '')) return true
return ['moderator', 'manager'].includes(getCommunityRole(user) || '')
}
/**
* Prüft ob User Zugriff auf Community-Features hat (mindestens Viewer)
*/
export const hasCommunityAccess: Access = ({ req }) => {
const user = req.user as UserWithRoles | null
if (!user) return false
if (checkIsSuperAdmin(user)) return true
// YouTube-Zugriff impliziert Community-Lesezugriff
const ytRole = getYouTubeRole(user)
if (ytRole && ytRole !== 'none') return true
const commRole = getCommunityRole(user)
return commRole !== 'none' && commRole !== undefined
}
/**
* Zugriff auf zugewiesene Interactions
*/
export const canAccessAssignedInteractions: Access = ({ req }) => {
const user = req.user as UserWithRoles | null
if (!user) return false
if (checkIsSuperAdmin(user)) return true
if (getYouTubeRole(user) === 'manager') return true
if (getCommunityRole(user) === 'manager') return true
// Für andere Rollen: Nur zugewiesene Interactions
return {
or: [
{ assignedTo: { equals: user.id } },
{ 'response.sentBy': { equals: user.id } },
],
}
}
```
---
## Teil 2: Collections
### 2.1 Social Platforms (`social-platforms`)
```typescript
// src/collections/SocialPlatforms.ts
import type { CollectionConfig } from 'payload'
import { isCommunityManager, hasCommunityAccess } from '../lib/communityAccess'
export const SocialPlatforms: CollectionConfig = {
slug: 'social-platforms',
labels: {
singular: 'Social Platform',
plural: 'Social Platforms',
},
admin: {
group: 'Community',
useAsTitle: 'name',
defaultColumns: ['name', 'slug', 'isActive', 'apiStatus'],
},
access: {
read: hasCommunityAccess,
create: isCommunityManager,
update: isCommunityManager,
delete: isCommunityManager,
},
fields: [
{
type: 'row',
fields: [
{
name: 'name',
type: 'text',
required: true,
label: 'Name',
admin: { width: '50%' },
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
label: 'Slug',
admin: { width: '50%' },
},
],
},
{
type: 'row',
fields: [
{
name: 'icon',
type: 'text',
label: 'Icon (Emoji)',
admin: {
width: '25%',
placeholder: '📺',
},
},
{
name: 'color',
type: 'text',
label: 'Brand Color',
admin: {
width: '25%',
placeholder: '#FF0000',
},
},
{
name: 'isActive',
type: 'checkbox',
label: 'Aktiv',
defaultValue: true,
admin: { width: '25%' },
},
{
name: 'apiStatus',
type: 'select',
label: 'API Status',
options: [
{ label: 'Verbunden', value: 'connected' },
{ label: 'Eingeschränkt', value: 'limited' },
{ label: 'Nicht verbunden', value: 'disconnected' },
{ label: 'In Entwicklung', value: 'development' },
],
defaultValue: 'disconnected',
admin: { width: '25%' },
},
],
},
// API Configuration
{
name: 'apiConfig',
type: 'group',
label: 'API Konfiguration',
admin: {
condition: (data) => data?.isActive,
},
fields: [
{
name: 'apiType',
type: 'select',
label: 'API Type',
options: [
{ label: 'YouTube Data API v3', value: 'youtube_v3' },
{ label: 'LinkedIn API', value: 'linkedin' },
{ label: 'Instagram Graph API', value: 'instagram_graph' },
{ label: 'Facebook Graph API', value: 'facebook_graph' },
{ label: 'Custom/Webhook', value: 'custom' },
],
},
{
name: 'baseUrl',
type: 'text',
label: 'Base URL',
admin: {
placeholder: 'https://www.googleapis.com/youtube/v3',
},
},
{
name: 'authType',
type: 'select',
label: 'Auth Type',
options: [
{ label: 'OAuth 2.0', value: 'oauth2' },
{ label: 'API Key', value: 'api_key' },
{ label: 'Bearer Token', value: 'bearer' },
],
},
{
name: 'scopes',
type: 'array',
label: 'OAuth Scopes',
admin: {
condition: (data, siblingData) => siblingData?.authType === 'oauth2',
},
fields: [
{ name: 'scope', type: 'text', label: 'Scope' },
],
},
],
},
// Interaction Types für diese Plattform
{
name: 'interactionTypes',
type: 'array',
label: 'Interaction Types',
fields: [
{
type: 'row',
fields: [
{
name: 'type',
type: 'text',
required: true,
label: 'Type',
admin: {
width: '30%',
placeholder: 'comment',
},
},
{
name: 'label',
type: 'text',
required: true,
label: 'Label',
admin: {
width: '30%',
placeholder: 'Kommentar',
},
},
{
name: 'icon',
type: 'text',
label: 'Icon',
admin: {
width: '20%',
placeholder: '💬',
},
},
{
name: 'canReply',
type: 'checkbox',
label: 'Reply möglich',
defaultValue: true,
admin: { width: '20%' },
},
],
},
],
},
// Rate Limits
{
name: 'rateLimits',
type: 'group',
label: 'Rate Limits',
fields: [
{
type: 'row',
fields: [
{
name: 'requestsPerMinute',
type: 'number',
label: 'Requests/Minute',
admin: { width: '33%' },
},
{
name: 'requestsPerDay',
type: 'number',
label: 'Requests/Tag',
admin: { width: '33%' },
},
{
name: 'quotaUnitsPerDay',
type: 'number',
label: 'Quota Units/Tag',
admin: {
width: '33%',
description: 'YouTube: 10.000/Tag',
},
},
],
},
],
},
],
timestamps: true,
}
```
### 2.2 Social Accounts (`social-accounts`)
```typescript
// src/collections/SocialAccounts.ts
import type { CollectionConfig } from 'payload'
import { isCommunityManager, hasCommunityAccess } from '../lib/communityAccess'
export const SocialAccounts: CollectionConfig = {
slug: 'social-accounts',
labels: {
singular: 'Social Account',
plural: 'Social Accounts',
},
admin: {
group: 'Community',
useAsTitle: 'displayName',
defaultColumns: ['displayName', 'platform', 'linkedChannel', 'isActive'],
},
access: {
read: hasCommunityAccess,
create: isCommunityManager,
update: isCommunityManager,
delete: isCommunityManager,
},
fields: [
{
type: 'row',
fields: [
{
name: 'platform',
type: 'relationship',
relationTo: 'social-platforms',
required: true,
label: 'Plattform',
admin: { width: '50%' },
},
{
name: 'linkedChannel',
type: 'relationship',
relationTo: 'youtube-channels',
label: 'Verknüpfter YouTube-Kanal',
admin: {
width: '50%',
description: 'Für Zuordnung zu Brand/Kanal',
},
},
],
},
{
type: 'row',
fields: [
{
name: 'displayName',
type: 'text',
required: true,
label: 'Anzeigename',
admin: {
width: '50%',
placeholder: 'BlogWoman YouTube',
},
},
{
name: 'accountHandle',
type: 'text',
label: 'Handle / Username',
admin: {
width: '50%',
placeholder: '@blogwoman',
},
},
],
},
{
type: 'row',
fields: [
{
name: 'externalId',
type: 'text',
label: 'Platform Account ID',
admin: {
width: '50%',
description: 'YouTube Channel ID, LinkedIn URN, etc.',
},
},
{
name: 'accountUrl',
type: 'text',
label: 'Account URL',
admin: { width: '50%' },
},
],
},
{
name: 'isActive',
type: 'checkbox',
label: 'Aktiv',
defaultValue: true,
admin: {
position: 'sidebar',
},
},
// OAuth Credentials (verschlüsselt speichern!)
{
name: 'credentials',
type: 'group',
label: 'API Credentials',
admin: {
description: 'Sensible Daten nur für Admins sichtbar',
condition: (data, siblingData, { user }) =>
Boolean((user as any)?.isSuperAdmin || (user as any)?.is_super_admin),
},
fields: [
{
name: 'accessToken',
type: 'text',
label: 'Access Token',
admin: {
description: 'OAuth Access Token',
},
},
{
name: 'refreshToken',
type: 'text',
label: 'Refresh Token',
},
{
name: 'tokenExpiresAt',
type: 'date',
label: 'Token Ablauf',
},
{
name: 'apiKey',
type: 'text',
label: 'API Key',
admin: {
description: 'Für API-Key basierte Auth',
},
},
],
},
// Stats (periodisch aktualisiert)
{
name: 'stats',
type: 'group',
label: 'Account Stats',
admin: {
position: 'sidebar',
},
fields: [
{
name: 'followers',
type: 'number',
label: 'Followers/Subscribers',
admin: { readOnly: true },
},
{
name: 'totalPosts',
type: 'number',
label: 'Total Posts/Videos',
admin: { readOnly: true },
},
{
name: 'lastSyncedAt',
type: 'date',
label: 'Letzter Sync',
admin: { readOnly: true },
},
],
},
// Sync Settings
{
name: 'syncSettings',
type: 'group',
label: 'Sync Settings',
fields: [
{
name: 'autoSyncEnabled',
type: 'checkbox',
label: 'Auto-Sync aktiviert',
defaultValue: true,
},
{
name: 'syncIntervalMinutes',
type: 'number',
label: 'Sync-Intervall (Minuten)',
defaultValue: 15,
min: 5,
max: 1440,
},
{
name: 'syncComments',
type: 'checkbox',
label: 'Kommentare synchronisieren',
defaultValue: true,
},
{
name: 'syncDMs',
type: 'checkbox',
label: 'DMs synchronisieren',
defaultValue: false,
admin: {
description: 'Nicht alle Plattformen unterstützen DM-API',
},
},
],
},
],
timestamps: true,
}
```
### 2.3 Community Interactions (`community-interactions`)
```typescript
// src/collections/CommunityInteractions.ts
import type { CollectionConfig } from 'payload'
import {
hasCommunityAccess,
isCommunityModeratorOrAbove,
isCommunityManager,
canAccessAssignedInteractions
} from '../lib/communityAccess'
export const CommunityInteractions: CollectionConfig = {
slug: 'community-interactions',
labels: {
singular: 'Interaction',
plural: 'Interactions',
},
admin: {
group: 'Community',
defaultColumns: ['platform', 'type', 'authorName', 'status', 'priority', 'createdAt'],
listSearchableFields: ['author.name', 'author.handle', 'message'],
pagination: {
defaultLimit: 50,
},
},
access: {
read: canAccessAssignedInteractions,
create: isCommunityModeratorOrAbove,
update: canAccessAssignedInteractions,
delete: isCommunityManager,
},
fields: [
// === SOURCE ===
{
type: 'row',
fields: [
{
name: 'platform',
type: 'relationship',
relationTo: 'social-platforms',
required: true,
label: 'Plattform',
admin: { width: '33%' },
},
{
name: 'socialAccount',
type: 'relationship',
relationTo: 'social-accounts',
required: true,
label: 'Account',
admin: { width: '33%' },
},
{
name: 'linkedContent',
type: 'relationship',
relationTo: 'youtube-content',
label: 'Verknüpfter Content',
admin: { width: '33%' },
},
],
},
// === INTERACTION TYPE ===
{
type: 'row',
fields: [
{
name: 'type',
type: 'select',
required: true,
label: 'Typ',
options: [
{ label: 'Kommentar', value: 'comment' },
{ label: 'Antwort', value: 'reply' },
{ label: 'Direktnachricht', value: 'dm' },
{ label: 'Erwähnung', value: 'mention' },
{ label: 'Bewertung', value: 'review' },
{ label: 'Frage', value: 'question' },
],
admin: { width: '50%' },
},
{
name: 'externalId',
type: 'text',
label: 'External ID',
required: true,
unique: true,
index: true,
admin: {
width: '50%',
description: 'YouTube Comment ID, etc.',
},
},
],
},
// === PARENT (für Threads) ===
{
name: 'parentInteraction',
type: 'relationship',
relationTo: 'community-interactions',
label: 'Parent (bei Replies)',
admin: {
condition: (data) => data?.type === 'reply',
},
},
// === AUTHOR INFO ===
{
name: 'author',
type: 'group',
label: 'Author',
fields: [
{
type: 'row',
fields: [
{
name: 'name',
type: 'text',
label: 'Name',
admin: { width: '50%' },
},
{
name: 'handle',
type: 'text',
label: 'Handle',
admin: { width: '50%' },
},
],
},
{
type: 'row',
fields: [
{
name: 'profileUrl',
type: 'text',
label: 'Profile URL',
admin: { width: '50%' },
},
{
name: 'avatarUrl',
type: 'text',
label: 'Avatar URL',
admin: { width: '50%' },
},
],
},
{
type: 'row',
fields: [
{
name: 'isVerified',
type: 'checkbox',
label: 'Verifiziert',
admin: { width: '25%' },
},
{
name: 'isSubscriber',
type: 'checkbox',
label: 'Subscriber/Follower',
admin: { width: '25%' },
},
{
name: 'isMember',
type: 'checkbox',
label: 'Channel Member',
admin: { width: '25%' },
},
{
name: 'subscriberCount',
type: 'number',
label: 'Ihre Subscriber',
admin: { width: '25%' },
},
],
},
],
},
// === MESSAGE CONTENT ===
{
name: 'message',
type: 'textarea',
label: 'Nachricht',
required: true,
admin: {
rows: 4,
},
},
{
name: 'messageHtml',
type: 'textarea',
label: 'Original HTML',
admin: {
rows: 2,
description: 'Falls Plattform HTML liefert',
},
},
{
name: 'attachments',
type: 'array',
label: 'Attachments',
fields: [
{
type: 'row',
fields: [
{
name: 'type',
type: 'select',
label: 'Typ',
options: [
{ label: 'Bild', value: 'image' },
{ label: 'Video', value: 'video' },
{ label: 'Link', value: 'link' },
{ label: 'Sticker', value: 'sticker' },
],
admin: { width: '30%' },
},
{
name: 'url',
type: 'text',
label: 'URL',
admin: { width: '70%' },
},
],
},
],
},
{
name: 'publishedAt',
type: 'date',
label: 'Veröffentlicht am',
required: true,
admin: {
date: {
pickerAppearance: 'dayAndTime',
},
},
},
// === AI ANALYSIS ===
{
name: 'analysis',
type: 'group',
label: 'AI Analyse',
admin: {
description: 'Automatisch via Claude API',
},
fields: [
{
type: 'row',
fields: [
{
name: 'sentiment',
type: 'select',
label: 'Sentiment',
options: [
{ label: 'Positiv', value: 'positive' },
{ label: 'Neutral', value: 'neutral' },
{ label: 'Negativ', value: 'negative' },
{ label: 'Frage', value: 'question' },
{ label: 'Dankbarkeit', value: 'gratitude' },
{ label: 'Frustration', value: 'frustration' },
],
admin: { width: '33%' },
},
{
name: 'sentimentScore',
type: 'number',
label: 'Score (-1 bis 1)',
min: -1,
max: 1,
admin: { width: '33%' },
},
{
name: 'confidence',
type: 'number',
label: 'Confidence %',
min: 0,
max: 100,
admin: { width: '33%' },
},
],
},
{
name: 'topics',
type: 'array',
label: 'Erkannte Themen',
fields: [
{
name: 'topic',
type: 'text',
label: 'Thema',
},
],
},
{
name: 'language',
type: 'text',
label: 'Sprache',
admin: {
placeholder: 'de',
},
},
{
name: 'suggestedTemplate',
type: 'relationship',
relationTo: 'community-templates',
label: 'Vorgeschlagenes Template',
},
{
name: 'suggestedReply',
type: 'textarea',
label: 'AI-generierter Antwortvorschlag',
},
{
name: 'analyzedAt',
type: 'date',
label: 'Analysiert am',
},
],
},
// === FLAGS ===
{
name: 'flags',
type: 'group',
label: 'Flags',
fields: [
{
type: 'row',
fields: [
{
name: 'isMedicalQuestion',
type: 'checkbox',
label: 'Medizinische Frage',
admin: {
width: '25%',
description: 'Erfordert ärztliche Review',
},
},
{
name: 'requiresEscalation',
type: 'checkbox',
label: 'Eskalation nötig',
admin: { width: '25%' },
},
{
name: 'isSpam',
type: 'checkbox',
label: 'Spam',
admin: { width: '25%' },
},
{
name: 'isFromInfluencer',
type: 'checkbox',
label: 'Influencer',
admin: {
width: '25%',
description: '>10k Follower',
},
},
],
},
],
},
// === WORKFLOW ===
{
name: 'status',
type: 'select',
required: true,
defaultValue: 'new',
index: true,
label: 'Status',
options: [
{ label: 'Neu', value: 'new' },
{ label: 'In Review', value: 'in_review' },
{ label: 'Warten auf Info', value: 'waiting' },
{ label: 'Beantwortet', value: 'replied' },
{ label: 'Erledigt', value: 'resolved' },
{ label: 'Archiviert', value: 'archived' },
{ label: 'Spam', value: 'spam' },
],
admin: {
position: 'sidebar',
},
},
{
name: 'priority',
type: 'select',
required: true,
defaultValue: 'normal',
index: true,
label: 'Priorität',
options: [
{ label: 'Urgent', value: 'urgent' },
{ label: 'Hoch', value: 'high' },
{ label: 'Normal', value: 'normal' },
{ label: 'Niedrig', value: 'low' },
],
admin: {
position: 'sidebar',
},
},
{
name: 'assignedTo',
type: 'relationship',
relationTo: 'users',
label: 'Zugewiesen an',
admin: {
position: 'sidebar',
},
},
{
name: 'responseDeadline',
type: 'date',
label: 'Antwort-Deadline',
admin: {
position: 'sidebar',
date: {
pickerAppearance: 'dayAndTime',
},
},
},
// === OUR RESPONSE ===
{
name: 'response',
type: 'group',
label: 'Unsere Antwort',
fields: [
{
name: 'text',
type: 'textarea',
label: 'Antwort-Text',
admin: { rows: 4 },
},
{
name: 'usedTemplate',
type: 'relationship',
relationTo: 'community-templates',
label: 'Verwendetes Template',
},
{
name: 'sentAt',
type: 'date',
label: 'Gesendet am',
},
{
name: 'sentBy',
type: 'relationship',
relationTo: 'users',
label: 'Gesendet von',
},
{
name: 'externalReplyId',
type: 'text',
label: 'Reply ID (extern)',
},
],
},
// === ENGAGEMENT (Platform-spezifisch) ===
{
name: 'engagement',
type: 'group',
label: 'Engagement',
admin: {
description: 'Wird beim Sync aktualisiert',
},
fields: [
{
type: 'row',
fields: [
{
name: 'likes',
type: 'number',
label: 'Likes',
admin: { width: '25%' },
},
{
name: 'replies',
type: 'number',
label: 'Replies',
admin: { width: '25%' },
},
{
name: 'isHearted',
type: 'checkbox',
label: 'Creator Heart',
admin: { width: '25%' },
},
{
name: 'isPinned',
type: 'checkbox',
label: 'Angepinnt',
admin: { width: '25%' },
},
],
},
],
},
// === INTERNAL NOTES ===
{
name: 'internalNotes',
type: 'textarea',
label: 'Interne Notizen',
admin: {
rows: 2,
description: 'Nur für Team sichtbar',
},
},
],
// === HOOKS ===
hooks: {
beforeChange: [
// Auto-set priority based on flags
async ({ data, operation }) => {
if (!data) return data
if (operation === 'create' || !data.priority) {
if (data?.flags?.isMedicalQuestion) {
data.priority = 'high'
}
if (data?.flags?.requiresEscalation) {
data.priority = 'urgent'
}
if (data?.flags?.isFromInfluencer) {
data.priority = data.priority === 'urgent' ? 'urgent' : 'high'
}
}
return data
},
],
afterChange: [
// Send notification for urgent items
async ({ doc, operation }) => {
if (operation === 'create' && doc.priority === 'urgent') {
// TODO: Notification Logic
console.log(`🚨 Urgent interaction: ${doc.id}`)
}
},
],
},
timestamps: true,
}
```
### 2.4 Community Templates (`community-templates`)
```typescript
// src/collections/CommunityTemplates.ts
import type { CollectionConfig } from 'payload'
import { isCommunityManager, isCommunityModeratorOrAbove, hasCommunityAccess } from '../lib/communityAccess'
export const CommunityTemplates: CollectionConfig = {
slug: 'community-templates',
labels: {
singular: 'Response Template',
plural: 'Response Templates',
},
admin: {
group: 'Community',
useAsTitle: 'name',
defaultColumns: ['name', 'category', 'channel', 'usageCount'],
},
access: {
read: hasCommunityAccess,
create: isCommunityModeratorOrAbove,
update: isCommunityModeratorOrAbove,
delete: isCommunityManager,
},
fields: [
{
type: 'row',
fields: [
{
name: 'name',
type: 'text',
required: true,
localized: true,
label: 'Name',
admin: { width: '50%' },
},
{
name: 'category',
type: 'select',
required: true,
label: 'Kategorie',
options: [
{ label: 'Danke', value: 'thank_you' },
{ label: 'Frage beantworten', value: 'question_answer' },
{ label: 'Hotline-Verweis', value: 'redirect_hotline' },
{ label: 'Medizinischer Disclaimer', value: 'medical_disclaimer' },
{ label: 'Produkt-Info', value: 'product_info' },
{ label: 'Content-Verweis', value: 'content_reference' },
{ label: 'Follow-up', value: 'follow_up' },
{ label: 'Negatives Feedback', value: 'negative_feedback' },
{ label: 'Spam-Antwort', value: 'spam_response' },
{ label: 'Begrüßung', value: 'welcome' },
],
admin: { width: '50%' },
},
],
},
{
type: 'row',
fields: [
{
name: 'channel',
type: 'relationship',
relationTo: 'youtube-channels',
label: 'Kanal (optional)',
admin: {
width: '50%',
description: 'Leer = für alle Kanäle',
},
},
{
name: 'platforms',
type: 'relationship',
relationTo: 'social-platforms',
hasMany: true,
label: 'Plattformen',
admin: {
width: '50%',
description: 'Leer = für alle Plattformen',
},
},
],
},
// Template Text mit Variablen
{
name: 'template',
type: 'textarea',
required: true,
localized: true,
label: 'Template Text',
admin: {
rows: 6,
description: 'Variablen: {{author_name}}, {{video_title}}, {{channel_name}}, {{hotline_number}}',
},
},
// Verfügbare Variablen
{
name: 'variables',
type: 'array',
label: 'Verfügbare Variablen',
admin: {
description: 'Dokumentation der Variablen in diesem Template',
},
fields: [
{
type: 'row',
fields: [
{
name: 'variable',
type: 'text',
required: true,
label: 'Variable',
admin: {
width: '30%',
placeholder: '{{author_name}}',
},
},
{
name: 'description',
type: 'text',
label: 'Beschreibung',
admin: {
width: '50%',
placeholder: 'Name des Kommentar-Autors',
},
},
{
name: 'defaultValue',
type: 'text',
label: 'Fallback',
admin: {
width: '20%',
},
},
],
},
],
},
// Auto-Suggest Keywords
{
name: 'autoSuggestKeywords',
type: 'array',
label: 'Auto-Suggest Keywords',
admin: {
description: 'Bei diesen Keywords wird das Template vorgeschlagen',
},
fields: [
{
name: 'keyword',
type: 'text',
required: true,
label: 'Keyword',
},
],
},
// Flags
{
type: 'row',
fields: [
{
name: 'requiresReview',
type: 'checkbox',
label: 'Review erforderlich',
admin: {
width: '33%',
description: 'Für medizinische Antworten',
},
},
{
name: 'isActive',
type: 'checkbox',
label: 'Aktiv',
defaultValue: true,
admin: { width: '33%' },
},
{
name: 'usageCount',
type: 'number',
label: 'Verwendungen',
defaultValue: 0,
admin: {
width: '33%',
readOnly: true,
},
},
],
},
// Beispiel-Output
{
name: 'exampleOutput',
type: 'textarea',
label: 'Beispiel-Output',
admin: {
rows: 3,
description: 'So sieht die Antwort mit ausgefüllten Variablen aus',
},
},
],
timestamps: true,
}
```
### 2.5 Community Rules (`community-rules`)
```typescript
// src/collections/CommunityRules.ts
import type { CollectionConfig } from 'payload'
import { isCommunityManager, hasCommunityAccess } from '../lib/communityAccess'
export const CommunityRules: CollectionConfig = {
slug: 'community-rules',
labels: {
singular: 'Community Rule',
plural: 'Community Rules',
},
admin: {
group: 'Community',
useAsTitle: 'name',
defaultColumns: ['name', 'trigger.type', 'isActive', 'priority'],
},
access: {
read: hasCommunityAccess,
create: isCommunityManager,
update: isCommunityManager,
delete: isCommunityManager,
},
fields: [
{
type: 'row',
fields: [
{
name: 'name',
type: 'text',
required: true,
label: 'Name',
admin: { width: '50%' },
},
{
name: 'priority',
type: 'number',
required: true,
defaultValue: 100,
label: 'Priorität',
admin: {
width: '25%',
description: 'Niedrigere Zahl = höhere Priorität',
},
},
{
name: 'isActive',
type: 'checkbox',
label: 'Aktiv',
defaultValue: true,
admin: { width: '25%' },
},
],
},
{
name: 'description',
type: 'textarea',
label: 'Beschreibung',
admin: { rows: 2 },
},
// Scope
{
type: 'row',
fields: [
{
name: 'channel',
type: 'relationship',
relationTo: 'youtube-channels',
label: 'Kanal (optional)',
admin: {
width: '50%',
description: 'Leer = alle Kanäle',
},
},
{
name: 'platforms',
type: 'relationship',
relationTo: 'social-platforms',
hasMany: true,
label: 'Plattformen',
admin: {
width: '50%',
description: 'Leer = alle Plattformen',
},
},
],
},
// Trigger
{
name: 'trigger',
type: 'group',
label: 'Trigger',
fields: [
{
name: 'type',
type: 'select',
required: true,
label: 'Trigger-Typ',
options: [
{ label: 'Keyword Match', value: 'keyword' },
{ label: 'Sentiment', value: 'sentiment' },
{ label: 'Frage erkannt', value: 'question_detected' },
{ label: 'Medizinisch erkannt', value: 'medical_detected' },
{ label: 'Influencer', value: 'influencer' },
{ label: 'Alle neuen', value: 'all_new' },
{ label: 'Enthält Link', value: 'contains_link' },
{ label: 'Enthält Email', value: 'contains_email' },
],
},
{
name: 'keywords',
type: 'array',
label: 'Keywords',
admin: {
condition: (data, siblingData) => siblingData?.type === 'keyword',
},
fields: [
{
name: 'keyword',
type: 'text',
required: true,
label: 'Keyword',
},
{
name: 'matchType',
type: 'select',
label: 'Match-Typ',
options: [
{ label: 'Enthält', value: 'contains' },
{ label: 'Exakt', value: 'exact' },
{ label: 'Regex', value: 'regex' },
],
defaultValue: 'contains',
},
],
},
{
name: 'sentimentValues',
type: 'select',
hasMany: true,
label: 'Sentiment-Werte',
options: [
{ label: 'Positiv', value: 'positive' },
{ label: 'Negativ', value: 'negative' },
{ label: 'Neutral', value: 'neutral' },
{ label: 'Frage', value: 'question' },
],
admin: {
condition: (data, siblingData) => siblingData?.type === 'sentiment',
},
},
{
name: 'influencerMinFollowers',
type: 'number',
label: 'Min. Follower',
defaultValue: 10000,
admin: {
condition: (data, siblingData) => siblingData?.type === 'influencer',
},
},
],
},
// Actions
{
name: 'actions',
type: 'array',
label: 'Aktionen',
required: true,
minRows: 1,
fields: [
{
type: 'row',
fields: [
{
name: 'action',
type: 'select',
required: true,
label: 'Aktion',
options: [
{ label: 'Priorität setzen', value: 'set_priority' },
{ label: 'Zuweisen', value: 'assign_to' },
{ label: 'Flag setzen', value: 'set_flag' },
{ label: 'Template vorschlagen', value: 'suggest_template' },
{ label: 'Notification senden', value: 'send_notification' },
{ label: 'Medical Flag', value: 'flag_medical' },
{ label: 'Eskalieren', value: 'escalate' },
{ label: 'Als Spam markieren', value: 'mark_spam' },
{ label: 'Deadline setzen', value: 'set_deadline' },
],
admin: { width: '40%' },
},
{
name: 'value',
type: 'text',
label: 'Wert',
admin: {
width: '40%',
description: 'Priority: urgent/high/normal/low, Deadline: Stunden',
},
},
{
name: 'targetUser',
type: 'relationship',
relationTo: 'users',
label: 'User',
admin: {
width: '20%',
condition: (data, siblingData) =>
['assign_to', 'send_notification'].includes(siblingData?.action || ''),
},
},
],
},
{
name: 'targetTemplate',
type: 'relationship',
relationTo: 'community-templates',
label: 'Template',
admin: {
condition: (data, siblingData) => siblingData?.action === 'suggest_template',
},
},
],
},
// Stats
{
name: 'stats',
type: 'group',
label: 'Statistiken',
fields: [
{
type: 'row',
fields: [
{
name: 'timesTriggered',
type: 'number',
label: 'Ausgelöst',
defaultValue: 0,
admin: { width: '50%', readOnly: true },
},
{
name: 'lastTriggeredAt',
type: 'date',
label: 'Zuletzt ausgelöst',
admin: { width: '50%', readOnly: true },
},
],
},
],
},
],
timestamps: true,
}
```
---
## Teil 3: Users Collection erweitern
### 3.1 Community-Rolle zu Users hinzufügen
```typescript
// Ergänzung in src/collections/Users.ts (zu den bestehenden YouTube-Feldern)
{
name: 'communityRole',
type: 'select',
label: 'Community-Rolle',
defaultValue: 'none',
options: [
{ label: 'Keine', value: 'none' },
{ label: 'Viewer', value: 'viewer' },
{ label: 'Moderator', value: 'moderator' },
{ label: 'Manager', value: 'manager' },
],
admin: {
position: 'sidebar',
description: 'Zugriff auf Community Management Features',
},
},
```
---
## Teil 4: YouTube API Integration
### 4.1 YouTube API Client
```typescript
// src/lib/integrations/youtube/YouTubeClient.ts
import { google, youtube_v3 } from 'googleapis'
import type { Payload } from 'payload'
interface YouTubeCredentials {
clientId: string
clientSecret: string
accessToken: string
refreshToken: string
}
interface CommentThread {
id: string
snippet: {
videoId: string
topLevelComment: {
id: string
snippet: {
textDisplay: string
textOriginal: string
authorDisplayName: string
authorProfileImageUrl: string
authorChannelUrl: string
authorChannelId: { value: string }
likeCount: number
publishedAt: string
updatedAt: string
}
}
totalReplyCount: number
}
}
export class YouTubeClient {
private youtube: youtube_v3.Youtube
private oauth2Client: any
private payload: Payload
constructor(credentials: YouTubeCredentials, payload: Payload) {
this.payload = payload
this.oauth2Client = new google.auth.OAuth2(
credentials.clientId,
credentials.clientSecret,
process.env.YOUTUBE_REDIRECT_URI
)
this.oauth2Client.setCredentials({
access_token: credentials.accessToken,
refresh_token: credentials.refreshToken,
})
this.youtube = google.youtube({
version: 'v3',
auth: this.oauth2Client,
})
}
/**
* Alle Kommentar-Threads eines Videos abrufen
*/
async getVideoComments(
videoId: string,
pageToken?: string,
maxResults: number = 100
): Promise<{
comments: CommentThread[]
nextPageToken?: string
}> {
try {
const response = await this.youtube.commentThreads.list({
part: ['snippet', 'replies'],
videoId: videoId,
maxResults: maxResults,
pageToken: pageToken,
order: 'time',
textFormat: 'plainText',
})
return {
comments: response.data.items as CommentThread[],
nextPageToken: response.data.nextPageToken || undefined,
}
} catch (error) {
console.error('Error fetching comments:', error)
throw error
}
}
/**
* Alle Kommentare eines Kanals abrufen (alle Videos)
*/
async getChannelComments(
channelId: string,
publishedAfter?: Date,
maxResults: number = 100
): Promise<CommentThread[]> {
try {
const params: any = {
part: ['snippet', 'replies'],
allThreadsRelatedToChannelId: channelId,
maxResults: maxResults,
order: 'time',
textFormat: 'plainText',
}
if (publishedAfter) {
params.publishedAfter = publishedAfter.toISOString()
}
const response = await this.youtube.commentThreads.list(params)
return response.data.items as CommentThread[]
} catch (error) {
console.error('Error fetching channel comments:', error)
throw error
}
}
/**
* Auf einen Kommentar antworten
*/
async replyToComment(parentCommentId: string, text: string): Promise<string> {
try {
const response = await this.youtube.comments.insert({
part: ['snippet'],
requestBody: {
snippet: {
parentId: parentCommentId,
textOriginal: text,
},
},
})
return response.data.id!
} catch (error) {
console.error('Error replying to comment:', error)
throw error
}
}
/**
* Kommentar als Spam markieren
*/
async markAsSpam(commentId: string): Promise<void> {
try {
await this.youtube.comments.setModerationStatus({
id: [commentId],
moderationStatus: 'rejected',
})
} catch (error) {
console.error('Error marking as spam:', error)
throw error
}
}
/**
* Kommentar löschen
*/
async deleteComment(commentId: string): Promise<void> {
try {
await this.youtube.comments.delete({
id: commentId,
})
} catch (error) {
console.error('Error deleting comment:', error)
throw error
}
}
/**
* Access Token erneuern
*/
async refreshAccessToken(): Promise<{
accessToken: string
expiresAt: Date
}> {
try {
const { credentials } = await this.oauth2Client.refreshAccessToken()
return {
accessToken: credentials.access_token!,
expiresAt: new Date(credentials.expiry_date!),
}
} catch (error) {
console.error('Error refreshing token:', error)
throw error
}
}
}
```
### 4.2 Claude Analysis Service
```typescript
// src/lib/integrations/claude/ClaudeAnalysisService.ts
import Anthropic from '@anthropic-ai/sdk'
interface CommentAnalysis {
sentiment: 'positive' | 'neutral' | 'negative' | 'question' | 'gratitude' | 'frustration'
sentimentScore: number
confidence: number
topics: string[]
language: string
isMedicalQuestion: boolean
requiresEscalation: boolean
isSpam: boolean
suggestedReply?: string
}
export class ClaudeAnalysisService {
private client: Anthropic
constructor() {
this.client = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
})
}
/**
* Kommentar analysieren
*/
async analyzeComment(message: string): Promise<CommentAnalysis> {
const systemPrompt = `Du bist ein Analyse-Assistent für YouTube-Kommentare eines deutschen Healthcare-Unternehmens (Medizinische Zweitmeinung) und eines Lifestyle-Kanals.
Analysiere den Kommentar und gib ein JSON-Objekt zurück mit:
- sentiment: "positive", "neutral", "negative", "question", "gratitude", "frustration"
- sentimentScore: Zahl von -1 (sehr negativ) bis 1 (sehr positiv)
- confidence: Konfidenz der Analyse 0-100
- topics: Array von erkannten Themen (max 3)
- language: ISO-639-1 Sprachcode (z.B. "de", "en")
- isMedicalQuestion: true wenn es um medizinische Fragen/Gesundheit geht
- requiresEscalation: true wenn dringend/kritisch/negativ oder Beschwerden
- isSpam: true wenn Spam/Werbung/Bot
- suggestedReply: Kurzer Antwortvorschlag auf Deutsch (optional, nur wenn sinnvoll)
WICHTIG für isMedicalQuestion:
- Fragen zu Intensivmedizin, Diagnosen, Behandlungen, Medikamenten = true
- Fragen zu Angehörigen von Patienten = true
- Allgemeine Lifestyle-Fragen (Mode, Zeitmanagement) = false
Antworte NUR mit dem JSON-Objekt, kein anderer Text.`
try {
const response = await this.client.messages.create({
model: 'claude-3-haiku-20240307',
max_tokens: 500,
system: systemPrompt,
messages: [
{
role: 'user',
content: `Analysiere diesen Kommentar:\n\n"${message}"`,
},
],
})
const content = response.content[0]
if (content.type !== 'text') {
throw new Error('Unexpected response type')
}
const analysis = JSON.parse(content.text) as CommentAnalysis
return analysis
} catch (error) {
console.error('Claude analysis error:', error)
// Fallback bei Fehler
return {
sentiment: 'neutral',
sentimentScore: 0,
confidence: 0,
topics: [],
language: 'de',
isMedicalQuestion: false,
requiresEscalation: false,
isSpam: false,
}
}
}
/**
* Antwort-Vorschlag generieren
*/
async generateReply(
comment: string,
context: {
videoTitle: string
channelName: string
isBusinessChannel: boolean
template?: string
}
): Promise<string> {
const systemPrompt = `Du bist ein Community-Manager für ${context.channelName}.
${context.isBusinessChannel
? 'Dies ist ein Healthcare-Kanal für medizinische Zweitmeinungen. Antworten müssen professionell sein und dürfen keine medizinischen Ratschläge geben. Bei medizinischen Fragen immer auf die Hotline verweisen.'
: 'Dies ist ein Lifestyle-Kanal für berufstätige Mütter. Antworten sollten warm, persönlich und hilfreich sein.'
}
Erstelle eine passende, kurze Antwort auf den Kommentar.
- Maximal 2-3 Sätze
- Persönlich und authentisch
- Auf Deutsch
${context.template ? `\nVerwende dieses Template als Basis:\n${context.template}` : ''}
Antworte NUR mit dem Antworttext, kein anderer Text.`
try {
const response = await this.client.messages.create({
model: 'claude-3-haiku-20240307',
max_tokens: 200,
system: systemPrompt,
messages: [
{
role: 'user',
content: `Video: "${context.videoTitle}"\n\nKommentar: "${comment}"\n\nErstelle eine Antwort:`,
},
],
})
const content = response.content[0]
if (content.type !== 'text') {
throw new Error('Unexpected response type')
}
return content.text.trim()
} catch (error) {
console.error('Claude reply generation error:', error)
throw error
}
}
}
```
---
## Teil 5: API Endpoints
### 5.1 Comments Sync Endpoint
```typescript
// src/app/(payload)/api/community/sync-comments/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@payload-config'
import { CommentsSyncService } from '@/lib/integrations/youtube/CommentsSyncService'
export async function POST(req: NextRequest) {
try {
const payload = await getPayload({ config })
const body = await req.json()
const { socialAccountId, sinceDate, maxComments, analyzeWithAI } = body
if (!socialAccountId) {
return NextResponse.json(
{ error: 'socialAccountId required' },
{ status: 400 }
)
}
const syncService = new CommentsSyncService(payload)
const result = await syncService.syncComments({
socialAccountId,
sinceDate: sinceDate ? new Date(sinceDate) : undefined,
maxComments: maxComments || 100,
analyzeWithAI: analyzeWithAI ?? true,
})
return NextResponse.json(result)
} catch (error: any) {
console.error('Sync error:', error)
return NextResponse.json(
{ error: error.message },
{ status: 500 }
)
}
}
```
### 5.2 Reply Endpoint
```typescript
// src/app/(payload)/api/community/reply/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@payload-config'
import { YouTubeClient } from '@/lib/integrations/youtube/YouTubeClient'
export async function POST(req: NextRequest) {
try {
const payload = await getPayload({ config })
const body = await req.json()
const { interactionId, replyText, templateId } = body
if (!interactionId || !replyText) {
return NextResponse.json(
{ error: 'interactionId and replyText required' },
{ status: 400 }
)
}
// 1. Interaction laden
const interaction = await payload.findByID({
collection: 'community-interactions',
id: interactionId,
depth: 2,
})
if (!interaction) {
return NextResponse.json(
{ error: 'Interaction not found' },
{ status: 404 }
)
}
// 2. YouTube Client initialisieren
const account = interaction.socialAccount as any
const youtubeClient = new YouTubeClient({
clientId: process.env.YOUTUBE_CLIENT_ID!,
clientSecret: process.env.YOUTUBE_CLIENT_SECRET!,
accessToken: account.credentials.accessToken,
refreshToken: account.credentials.refreshToken,
}, payload)
// 3. Reply senden
const replyId = await youtubeClient.replyToComment(
interaction.externalId,
replyText
)
// 4. Interaction aktualisieren
await payload.update({
collection: 'community-interactions',
id: interactionId,
data: {
status: 'replied',
response: {
text: replyText,
usedTemplate: templateId || null,
sentAt: new Date().toISOString(),
externalReplyId: replyId,
},
},
})
// 5. Template Usage Counter erhöhen
if (templateId) {
const template = await payload.findByID({
collection: 'community-templates',
id: templateId,
})
await payload.update({
collection: 'community-templates',
id: templateId,
data: {
usageCount: ((template as any).usageCount || 0) + 1,
},
})
}
return NextResponse.json({
success: true,
replyId,
})
} catch (error: any) {
console.error('Reply error:', error)
return NextResponse.json(
{ error: error.message },
{ status: 500 }
)
}
}
```
---
## Teil 6: Environment Variables
```bash
# .env - Neue Variablen für Community Phase 1
# YouTube API (Google Cloud Console)
YOUTUBE_CLIENT_ID=your-client-id.apps.googleusercontent.com
YOUTUBE_CLIENT_SECRET=your-client-secret
YOUTUBE_REDIRECT_URI=https://pl.porwoll.tech/api/youtube/callback
# Claude API (Anthropic) - bereits vorhanden für andere Features
ANTHROPIC_API_KEY=sk-ant-api03-...
# Optional: Für spätere Phasen vorbereitet
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
INSTAGRAM_ACCESS_TOKEN=
FACEBOOK_ACCESS_TOKEN=
```
---
## Teil 7: payload.config.ts Ergänzungen
```typescript
// payload.config.ts - Ergänzungen für Community Phase 1
import { SocialPlatforms } from './collections/SocialPlatforms'
import { SocialAccounts } from './collections/SocialAccounts'
import { CommunityInteractions } from './collections/CommunityInteractions'
import { CommunityTemplates } from './collections/CommunityTemplates'
import { CommunityRules } from './collections/CommunityRules'
export default buildConfig({
// ...
collections: [
// Existing YouTube Collections
YouTubeChannels,
YouTubeContent,
YtSeries,
YtTasks,
YtNotifications,
YtBatches,
YtMonthlyGoals,
YtScriptTemplates,
YtChecklistTemplates,
// NEW: Community Collections
SocialPlatforms,
SocialAccounts,
CommunityInteractions,
CommunityTemplates,
CommunityRules,
],
// ...
})
```
---
## Teil 8: Migration
Die Migration muss folgende Tabellen erstellen:
- `social_platforms` + `social_platforms_interaction_types` + `social_platforms_api_config_scopes`
- `social_accounts`
- `community_interactions` + `community_interactions_attachments` + `community_interactions_analysis_topics`
- `community_templates` + `community_templates_locales` + `community_templates_variables` + `community_templates_auto_suggest_keywords`
- `community_rules` + `community_rules_trigger_keywords` + `community_rules_actions`
- `payload_locked_documents_rels` erweitern um alle neuen Collection-IDs
**WICHTIG:** Siehe `CLAUDE.md` Abschnitt "KRITISCH: Neue Collections hinzufügen" für das korrekte Migrations-Muster.
---
## Zusammenfassung
### Neue Collections (5)
1. `social-platforms` Plattform-Definitionen
2. `social-accounts` Account-Verknüpfungen
3. `community-interactions` Alle Kommentare/DMs
4. `community-templates` Antwort-Vorlagen
5. `community-rules` Auto-Regeln
### Neue Lib-Dateien (3)
1. `src/lib/communityAccess.ts` Access Control
2. `src/lib/integrations/youtube/YouTubeClient.ts` YouTube API
3. `src/lib/integrations/claude/ClaudeAnalysisService.ts` AI Analyse
### API Endpoints (2)
1. `POST /api/community/sync-comments` Manueller Sync
2. `POST /api/community/reply` Antwort senden
### User-Feld (1)
1. `communityRole` in Users Collection
### Abhängigkeiten zu bestehendem Code
- `youtube-channels` Collection (linkedChannel Referenz)
- `youtube-content` Collection (linkedContent Referenz)
- `users` Collection (assignedTo, sentBy Referenzen)
- Bestehende YouTube-Access-Rollen werden berücksichtigt