Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
93
marketing-agent/.env.development
Normal file
93
marketing-agent/.env.development
Normal file
@@ -0,0 +1,93 @@
|
||||
# Development Environment Configuration
|
||||
# This file contains environment variables for local development
|
||||
|
||||
# Service URLs (local development)
|
||||
ORCHESTRATOR_URL=http://localhost:3001
|
||||
CLAUDE_AGENT_URL=http://localhost:3002
|
||||
GRAMJS_ADAPTER_URL=http://localhost:3003
|
||||
SAFETY_GUARD_URL=http://localhost:3004
|
||||
ANALYTICS_URL=http://localhost:3005
|
||||
COMPLIANCE_GUARD_URL=http://localhost:3006
|
||||
AB_TESTING_URL=http://localhost:3007
|
||||
|
||||
# Frontend URL (for CORS)
|
||||
FRONTEND_URL=http://localhost:3009
|
||||
|
||||
# API Gateway Configuration
|
||||
API_GATEWAY_PORT=3000
|
||||
JWT_SECRET=dev-jwt-secret-key
|
||||
JWT_EXPIRES_IN=24h
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
CORS_ORIGINS=http://localhost:3008,http://localhost:3009,http://localhost:5173
|
||||
|
||||
# Database Configuration
|
||||
MONGODB_URI=mongodb://localhost:27017/marketing_agent_dev
|
||||
POSTGRES_DB=marketing_agent_dev
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# RabbitMQ Configuration
|
||||
RABBITMQ_HOST=localhost
|
||||
RABBITMQ_PORT=5672
|
||||
RABBITMQ_USER=guest
|
||||
RABBITMQ_PASSWORD=guest
|
||||
|
||||
# Elasticsearch Configuration
|
||||
ELASTICSEARCH_HOST=localhost
|
||||
ELASTICSEARCH_PORT=9200
|
||||
|
||||
# ClickHouse Configuration
|
||||
CLICKHOUSE_HOST=localhost
|
||||
CLICKHOUSE_PORT=8123
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=debug
|
||||
LOG_FORMAT=pretty
|
||||
|
||||
# Rate Limiting (relaxed for development)
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX_REQUESTS=1000
|
||||
|
||||
# Claude AI Configuration (use test key for development)
|
||||
CLAUDE_API_KEY=test-key-replace-with-real-key
|
||||
CLAUDE_MODEL=claude-3-opus-20240229
|
||||
CLAUDE_MAX_TOKENS=4000
|
||||
|
||||
# Telegram Configuration (test credentials)
|
||||
TELEGRAM_API_ID=test-api-id
|
||||
TELEGRAM_API_HASH=test-api-hash
|
||||
TELEGRAM_SESSION_DIR=./sessions
|
||||
|
||||
# Safety Guard Configuration (relaxed for development)
|
||||
SAFETY_RATE_LIMIT_MESSAGES_PER_MINUTE=100
|
||||
SAFETY_RATE_LIMIT_MESSAGES_PER_HOUR=1000
|
||||
SAFETY_RATE_LIMIT_MESSAGES_PER_DAY=10000
|
||||
|
||||
# Analytics Configuration
|
||||
ANALYTICS_RETENTION_DAYS=30
|
||||
ANALYTICS_AGGREGATION_INTERVAL=60000
|
||||
|
||||
# A/B Testing Configuration
|
||||
AB_TESTING_MIN_SAMPLE_SIZE=10
|
||||
AB_TESTING_CONFIDENCE_LEVEL=0.95
|
||||
|
||||
# Compliance Configuration
|
||||
COMPLIANCE_DATA_RETENTION_DAYS=30
|
||||
COMPLIANCE_AUDIT_LOG_ENABLED=false
|
||||
|
||||
# Performance Configuration
|
||||
NODE_ENV=development
|
||||
NODE_OPTIONS=--max-old-space-size=2048
|
||||
|
||||
# Monitoring
|
||||
PROMETHEUS_ENABLED=false
|
||||
PROMETHEUS_PORT=9090
|
||||
GRAFANA_ENABLED=false
|
||||
GRAFANA_PORT=3020
|
||||
93
marketing-agent/.env.docker
Normal file
93
marketing-agent/.env.docker
Normal file
@@ -0,0 +1,93 @@
|
||||
# Docker Environment Configuration
|
||||
# This file contains environment variables for Docker Compose deployment
|
||||
|
||||
# Service URLs (internal Docker network)
|
||||
ORCHESTRATOR_URL=http://orchestrator:3001
|
||||
CLAUDE_AGENT_URL=http://claude-agent:3002
|
||||
GRAMJS_ADAPTER_URL=http://gramjs-adapter:3003
|
||||
SAFETY_GUARD_URL=http://safety-guard:3004
|
||||
ANALYTICS_URL=http://analytics:3005
|
||||
COMPLIANCE_GUARD_URL=http://compliance-guard:3006
|
||||
AB_TESTING_URL=http://ab-testing:3007
|
||||
|
||||
# Frontend URL (for CORS)
|
||||
FRONTEND_URL=http://localhost:3009
|
||||
|
||||
# API Gateway Configuration
|
||||
API_GATEWAY_PORT=3000
|
||||
JWT_SECRET=your-secure-jwt-secret-key-change-in-production
|
||||
JWT_EXPIRES_IN=24h
|
||||
JWT_REFRESH_EXPIRES_IN=7d
|
||||
CORS_ORIGINS=http://localhost:3008,http://localhost:3009
|
||||
|
||||
# Database Configuration
|
||||
MONGODB_URI=mongodb://mongodb:27017/marketing_agent
|
||||
POSTGRES_DB=marketing_agent
|
||||
POSTGRES_USER=marketing_user
|
||||
POSTGRES_PASSWORD=marketing_password
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# RabbitMQ Configuration
|
||||
RABBITMQ_HOST=rabbitmq
|
||||
RABBITMQ_PORT=5672
|
||||
RABBITMQ_USER=guest
|
||||
RABBITMQ_PASSWORD=guest
|
||||
|
||||
# Elasticsearch Configuration
|
||||
ELASTICSEARCH_HOST=elasticsearch
|
||||
ELASTICSEARCH_PORT=9200
|
||||
|
||||
# ClickHouse Configuration
|
||||
CLICKHOUSE_HOST=clickhouse
|
||||
CLICKHOUSE_PORT=8123
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
LOG_FORMAT=json
|
||||
|
||||
# Rate Limiting
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
|
||||
# Claude AI Configuration
|
||||
CLAUDE_API_KEY=your-claude-api-key-here
|
||||
CLAUDE_MODEL=claude-3-opus-20240229
|
||||
CLAUDE_MAX_TOKENS=4000
|
||||
|
||||
# Telegram Configuration
|
||||
TELEGRAM_API_ID=your-telegram-api-id
|
||||
TELEGRAM_API_HASH=your-telegram-api-hash
|
||||
TELEGRAM_SESSION_DIR=/app/sessions
|
||||
|
||||
# Safety Guard Configuration
|
||||
SAFETY_RATE_LIMIT_MESSAGES_PER_MINUTE=30
|
||||
SAFETY_RATE_LIMIT_MESSAGES_PER_HOUR=500
|
||||
SAFETY_RATE_LIMIT_MESSAGES_PER_DAY=5000
|
||||
|
||||
# Analytics Configuration
|
||||
ANALYTICS_RETENTION_DAYS=90
|
||||
ANALYTICS_AGGREGATION_INTERVAL=300000
|
||||
|
||||
# A/B Testing Configuration
|
||||
AB_TESTING_MIN_SAMPLE_SIZE=100
|
||||
AB_TESTING_CONFIDENCE_LEVEL=0.95
|
||||
|
||||
# Compliance Configuration
|
||||
COMPLIANCE_DATA_RETENTION_DAYS=365
|
||||
COMPLIANCE_AUDIT_LOG_ENABLED=true
|
||||
|
||||
# Performance Configuration
|
||||
NODE_ENV=production
|
||||
NODE_OPTIONS=--max-old-space-size=4096
|
||||
|
||||
# Monitoring
|
||||
PROMETHEUS_ENABLED=true
|
||||
PROMETHEUS_PORT=9090
|
||||
GRAFANA_ENABLED=true
|
||||
GRAFANA_PORT=3020
|
||||
56
marketing-agent/.env.example
Normal file
56
marketing-agent/.env.example
Normal file
@@ -0,0 +1,56 @@
|
||||
# MongoDB
|
||||
MONGODB_URI=mongodb://localhost:27017/marketing-agent
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# PostgreSQL
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=marketing_agent
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
|
||||
# Elasticsearch
|
||||
ELASTICSEARCH_URL=http://localhost:9200
|
||||
|
||||
# ClickHouse
|
||||
CLICKHOUSE_URL=http://localhost:8123
|
||||
CLICKHOUSE_DATABASE=marketing_agent
|
||||
|
||||
# JWT Secret
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
|
||||
JWT_REFRESH_SECRET=your-super-secret-refresh-key-change-this-in-production
|
||||
|
||||
# Telegram API (Get from https://my.telegram.org)
|
||||
TELEGRAM_API_ID=your-telegram-api-id
|
||||
TELEGRAM_API_HASH=your-telegram-api-hash
|
||||
|
||||
# Claude API
|
||||
CLAUDE_API_KEY=your-claude-api-key
|
||||
CLAUDE_MODEL=claude-3-opus-20240229
|
||||
|
||||
# Service Ports
|
||||
API_GATEWAY_PORT=3000
|
||||
ORCHESTRATOR_PORT=3001
|
||||
CLAUDE_AGENT_PORT=3002
|
||||
GRAMJS_ADAPTER_PORT=3003
|
||||
SAFETY_GUARD_PORT=3004
|
||||
ANALYTICS_PORT=3005
|
||||
AB_TESTING_PORT=3006
|
||||
COMPLIANCE_GUARD_PORT=3007
|
||||
|
||||
# Feature Flags
|
||||
ENABLE_DEMO_MODE=true
|
||||
ENABLE_REAL_TELEGRAM=false
|
||||
ENABLE_AI_SUGGESTIONS=false
|
||||
|
||||
# Rate Limiting
|
||||
RATE_LIMIT_WINDOW=900
|
||||
RATE_LIMIT_MAX=100
|
||||
|
||||
# Monitoring
|
||||
ENABLE_METRICS=true
|
||||
ENABLE_TRACING=true
|
||||
GRAFANA_URL=http://localhost:3030
|
||||
PROMETHEUS_URL=http://localhost:9090
|
||||
103
marketing-agent/.env.production.example
Normal file
103
marketing-agent/.env.production.example
Normal file
@@ -0,0 +1,103 @@
|
||||
# Production Environment Configuration Template
|
||||
# Copy this file to .env.production and fill in the values
|
||||
|
||||
# Application Settings
|
||||
NODE_ENV=production
|
||||
LOG_LEVEL=info
|
||||
PORT=3000
|
||||
|
||||
# Security
|
||||
JWT_SECRET=GENERATE_WITH_OPENSSL_RAND_BASE64_32
|
||||
JWT_EXPIRY=7d
|
||||
ENCRYPTION_KEY=GENERATE_32_BYTE_KEY
|
||||
CORS_ORIGIN=https://app.marketing-agent.com
|
||||
|
||||
# Database Configuration
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_USER=marketing_user
|
||||
POSTGRES_PASSWORD=CHANGE_THIS_SECURE_PASSWORD
|
||||
POSTGRES_DB=marketing_agent
|
||||
|
||||
MONGODB_URI=mongodb://marketing_user:CHANGE_THIS_SECURE_PASSWORD@mongodb:27017/marketing_agent?authSource=admin
|
||||
MONGO_USERNAME=marketing_user
|
||||
MONGO_PASSWORD=CHANGE_THIS_SECURE_PASSWORD
|
||||
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=CHANGE_THIS_SECURE_PASSWORD
|
||||
|
||||
# Message Queue
|
||||
RABBITMQ_URL=amqp://admin:CHANGE_THIS_SECURE_PASSWORD@rabbitmq:5672
|
||||
RABBITMQ_DEFAULT_USER=admin
|
||||
RABBITMQ_DEFAULT_PASS=CHANGE_THIS_SECURE_PASSWORD
|
||||
|
||||
# Elasticsearch
|
||||
ELASTICSEARCH_NODE=http://elasticsearch:9200
|
||||
ELASTIC_PASSWORD=CHANGE_THIS_SECURE_PASSWORD
|
||||
|
||||
# External Services
|
||||
ANTHROPIC_API_KEY=YOUR_ANTHROPIC_API_KEY
|
||||
OPENAI_API_KEY=YOUR_OPENAI_API_KEY
|
||||
GOOGLE_CLOUD_PROJECT=YOUR_GCP_PROJECT
|
||||
TELEGRAM_SYSTEM_URL=https://your-telegram-system-url.com
|
||||
|
||||
# Email Configuration
|
||||
SMTP_HOST=smtp.sendgrid.net
|
||||
SMTP_PORT=587
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=apikey
|
||||
SMTP_PASS=YOUR_SENDGRID_API_KEY
|
||||
EMAIL_FROM=noreply@marketing-agent.com
|
||||
|
||||
# Monitoring
|
||||
GRAFANA_ADMIN_PASSWORD=CHANGE_THIS_SECURE_PASSWORD
|
||||
PROMETHEUS_RETENTION=30d
|
||||
|
||||
# Backup Configuration
|
||||
BACKUP_S3_BUCKET=marketing-agent-backups
|
||||
BACKUP_S3_REGION=us-east-1
|
||||
BACKUP_RETENTION_DAYS=30
|
||||
AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY
|
||||
AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_KEY
|
||||
|
||||
# Rate Limiting
|
||||
RATE_LIMIT_WINDOW=60000
|
||||
RATE_LIMIT_MAX=100
|
||||
RATE_LIMIT_SKIP_SUCCESSFUL_REQUESTS=false
|
||||
RATE_LIMIT_SKIP_FAILED_REQUESTS=false
|
||||
|
||||
# Performance
|
||||
MAX_CONCURRENT_CAMPAIGNS=10
|
||||
MESSAGE_BATCH_SIZE=100
|
||||
WORKER_CONCURRENCY=4
|
||||
|
||||
# Feature Flags
|
||||
ENABLE_AI_SUGGESTIONS=true
|
||||
ENABLE_AUTO_COMPLIANCE=true
|
||||
ENABLE_ADVANCED_ANALYTICS=true
|
||||
ENABLE_WEBHOOK_INTEGRATIONS=true
|
||||
|
||||
# CDN Configuration
|
||||
CDN_URL=https://cdn.marketing-agent.com
|
||||
STATIC_ASSETS_URL=https://static.marketing-agent.com
|
||||
|
||||
# Sentry Error Tracking (Optional)
|
||||
SENTRY_DSN=YOUR_SENTRY_DSN
|
||||
SENTRY_ENVIRONMENT=production
|
||||
|
||||
# ClickHouse Analytics (Optional)
|
||||
CLICKHOUSE_HOST=clickhouse
|
||||
CLICKHOUSE_PORT=8123
|
||||
CLICKHOUSE_USER=default
|
||||
CLICKHOUSE_PASSWORD=CHANGE_THIS_SECURE_PASSWORD
|
||||
|
||||
# Compliance
|
||||
GDPR_MODE=true
|
||||
DATA_RETENTION_DAYS=365
|
||||
AUDIT_LOG_RETENTION_DAYS=2555
|
||||
|
||||
# Deployment
|
||||
DEPLOYMENT_REGION=us-east-1
|
||||
MULTI_REGION_ENABLED=true
|
||||
BLUE_GREEN_DEPLOYMENT=true
|
||||
348
marketing-agent/.github/workflows/cd.yml
vendored
Normal file
348
marketing-agent/.github/workflows/cd.yml
vendored
Normal file
@@ -0,0 +1,348 @@
|
||||
name: CD Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
tags: [ 'v*.*.*' ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: 'Deployment environment'
|
||||
required: true
|
||||
default: 'staging'
|
||||
type: choice
|
||||
options:
|
||||
- staging
|
||||
- production
|
||||
version:
|
||||
description: 'Version to deploy (leave empty for latest)'
|
||||
required: false
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
HELM_VERSION: '3.13.0'
|
||||
|
||||
jobs:
|
||||
# Prepare deployment
|
||||
prepare-deployment:
|
||||
name: Prepare Deployment
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
environment: ${{ steps.determine-env.outputs.environment }}
|
||||
version: ${{ steps.determine-version.outputs.version }}
|
||||
should-deploy: ${{ steps.check-deploy.outputs.should-deploy }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Determine environment
|
||||
id: determine-env
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
echo "environment=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||
echo "environment=staging" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ github.ref }}" =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "environment=production" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "environment=development" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Determine version
|
||||
id: determine-version
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" && -n "${{ github.event.inputs.version }}" ]]; then
|
||||
echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ github.ref }}" =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "version=${{ github.sha }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check deployment conditions
|
||||
id: check-deploy
|
||||
run: |
|
||||
ENV="${{ steps.determine-env.outputs.environment }}"
|
||||
if [[ "$ENV" == "production" && ! "${{ github.ref }}" =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "should-deploy=false" >> $GITHUB_OUTPUT
|
||||
echo "::warning::Production deployment requires a semantic version tag"
|
||||
else
|
||||
echo "should-deploy=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Database Migration
|
||||
database-migration:
|
||||
name: Database Migration
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare-deployment]
|
||||
if: needs.prepare-deployment.outputs.should-deploy == 'true'
|
||||
environment: ${{ needs.prepare-deployment.outputs.environment }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18.x'
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
|
||||
- name: Get database connection string
|
||||
id: get-db-connection
|
||||
run: |
|
||||
SECRET_ARN="${{ secrets.DB_SECRET_ARN }}"
|
||||
DB_SECRET=$(aws secretsmanager get-secret-value --secret-id $SECRET_ARN --query SecretString --output text)
|
||||
echo "::add-mask::$DB_SECRET"
|
||||
echo "DB_CONNECTION=$DB_SECRET" >> $GITHUB_ENV
|
||||
|
||||
- name: Run migrations
|
||||
run: |
|
||||
cd migrations
|
||||
npm ci
|
||||
npm run migrate:up
|
||||
env:
|
||||
MONGODB_URI: ${{ env.DB_CONNECTION }}
|
||||
MIGRATION_ENV: ${{ needs.prepare-deployment.outputs.environment }}
|
||||
|
||||
# Deploy to Kubernetes
|
||||
deploy-kubernetes:
|
||||
name: Deploy to Kubernetes
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare-deployment, database-migration]
|
||||
if: needs.prepare-deployment.outputs.should-deploy == 'true'
|
||||
environment:
|
||||
name: ${{ needs.prepare-deployment.outputs.environment }}
|
||||
url: ${{ steps.deploy.outputs.app-url }}
|
||||
strategy:
|
||||
matrix:
|
||||
region: [us-east-1, eu-west-1, ap-southeast-1]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ matrix.region }}
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@v2
|
||||
|
||||
- name: Setup kubectl
|
||||
uses: azure/setup-kubectl@v3
|
||||
with:
|
||||
version: 'v1.28.0'
|
||||
|
||||
- name: Setup Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: ${{ env.HELM_VERSION }}
|
||||
|
||||
- name: Update kubeconfig
|
||||
run: |
|
||||
aws eks update-kubeconfig --name marketing-agent-${{ needs.prepare-deployment.outputs.environment }}-${{ matrix.region }}
|
||||
|
||||
- name: Create namespace
|
||||
run: |
|
||||
kubectl create namespace marketing-agent-${{ needs.prepare-deployment.outputs.environment }} --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
- name: Deploy with Helm
|
||||
id: deploy
|
||||
run: |
|
||||
NAMESPACE="marketing-agent-${{ needs.prepare-deployment.outputs.environment }}"
|
||||
RELEASE_NAME="marketing-agent"
|
||||
|
||||
helm upgrade --install $RELEASE_NAME ./helm/marketing-agent \
|
||||
--namespace $NAMESPACE \
|
||||
--values ./helm/marketing-agent/values.yaml \
|
||||
--values ./helm/marketing-agent/values.${{ needs.prepare-deployment.outputs.environment }}.yaml \
|
||||
--set global.image.tag=${{ needs.prepare-deployment.outputs.version }} \
|
||||
--set global.image.registry=${{ steps.login-ecr.outputs.registry }} \
|
||||
--set global.region=${{ matrix.region }} \
|
||||
--set-string global.environment=${{ needs.prepare-deployment.outputs.environment }} \
|
||||
--wait \
|
||||
--timeout 10m
|
||||
|
||||
# Get the application URL
|
||||
APP_URL=$(kubectl get ingress -n $NAMESPACE -o jsonpath='{.items[0].spec.rules[0].host}')
|
||||
echo "app-url=https://$APP_URL" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
NAMESPACE="marketing-agent-${{ needs.prepare-deployment.outputs.environment }}"
|
||||
kubectl rollout status deployment -n $NAMESPACE --timeout=5m
|
||||
kubectl get pods -n $NAMESPACE
|
||||
|
||||
- name: Run smoke tests
|
||||
run: |
|
||||
APP_URL="${{ steps.deploy.outputs.app-url }}"
|
||||
./scripts/smoke-tests.sh $APP_URL
|
||||
|
||||
# Deploy Static Assets to CDN
|
||||
deploy-cdn:
|
||||
name: Deploy Static Assets to CDN
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare-deployment, database-migration]
|
||||
if: needs.prepare-deployment.outputs.should-deploy == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
env:
|
||||
VITE_API_URL: ${{ vars.API_URL }}
|
||||
VITE_ENVIRONMENT: ${{ needs.prepare-deployment.outputs.environment }}
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: us-east-1
|
||||
|
||||
- name: Deploy to S3
|
||||
run: |
|
||||
BUCKET_NAME="marketing-agent-frontend-${{ needs.prepare-deployment.outputs.environment }}"
|
||||
aws s3 sync ./frontend/dist s3://$BUCKET_NAME \
|
||||
--delete \
|
||||
--cache-control "public, max-age=31536000" \
|
||||
--exclude "index.html" \
|
||||
--exclude "*.map"
|
||||
|
||||
# Upload index.html with no-cache
|
||||
aws s3 cp ./frontend/dist/index.html s3://$BUCKET_NAME/index.html \
|
||||
--cache-control "no-cache, no-store, must-revalidate"
|
||||
|
||||
- name: Invalidate CloudFront
|
||||
run: |
|
||||
DISTRIBUTION_ID="${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }}"
|
||||
aws cloudfront create-invalidation \
|
||||
--distribution-id $DISTRIBUTION_ID \
|
||||
--paths "/*"
|
||||
|
||||
# Post-deployment tasks
|
||||
post-deployment:
|
||||
name: Post Deployment Tasks
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare-deployment, deploy-kubernetes, deploy-cdn]
|
||||
if: always() && needs.prepare-deployment.outputs.should-deploy == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Update deployment status
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const environment = '${{ needs.prepare-deployment.outputs.environment }}';
|
||||
const version = '${{ needs.prepare-deployment.outputs.version }}';
|
||||
const status = '${{ needs.deploy-kubernetes.result }}';
|
||||
|
||||
// Create deployment status
|
||||
await github.rest.repos.createDeploymentStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
deployment_id: context.payload.deployment.id,
|
||||
state: status === 'success' ? 'success' : 'failure',
|
||||
environment_url: status === 'success' ? '${{ needs.deploy-kubernetes.outputs.app-url }}' : '',
|
||||
description: `Deployed version ${version} to ${environment}`
|
||||
});
|
||||
|
||||
- name: Send deployment notification
|
||||
if: success()
|
||||
uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: custom
|
||||
custom_payload: |
|
||||
{
|
||||
"text": "Deployment Successful! :rocket:",
|
||||
"attachments": [{
|
||||
"color": "good",
|
||||
"fields": [
|
||||
{ "title": "Environment", "value": "${{ needs.prepare-deployment.outputs.environment }}", "short": true },
|
||||
{ "title": "Version", "value": "${{ needs.prepare-deployment.outputs.version }}", "short": true },
|
||||
{ "title": "Deployed By", "value": "${{ github.actor }}", "short": true },
|
||||
{ "title": "Repository", "value": "${{ github.repository }}", "short": true }
|
||||
]
|
||||
}]
|
||||
}
|
||||
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
|
||||
|
||||
- name: Create release notes
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const tag = context.ref.replace('refs/tags/', '');
|
||||
const { data: commits } = await github.rest.repos.compareCommits({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
base: 'v1.0.0', // Previous release tag
|
||||
head: tag
|
||||
});
|
||||
|
||||
const releaseNotes = commits.commits
|
||||
.map(commit => `- ${commit.commit.message}`)
|
||||
.join('\n');
|
||||
|
||||
await github.rest.repos.createRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag_name: tag,
|
||||
name: `Release ${tag}`,
|
||||
body: releaseNotes,
|
||||
draft: false,
|
||||
prerelease: false
|
||||
});
|
||||
|
||||
# Rollback on failure
|
||||
rollback:
|
||||
name: Rollback Deployment
|
||||
runs-on: ubuntu-latest
|
||||
needs: [prepare-deployment, deploy-kubernetes]
|
||||
if: failure() && needs.prepare-deployment.outputs.environment == 'production'
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ vars.AWS_REGION }}
|
||||
|
||||
- name: Rollback Kubernetes deployment
|
||||
run: |
|
||||
aws eks update-kubeconfig --name marketing-agent-production
|
||||
NAMESPACE="marketing-agent-production"
|
||||
helm rollback marketing-agent -n $NAMESPACE
|
||||
|
||||
- name: Send rollback notification
|
||||
uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: custom
|
||||
custom_payload: |
|
||||
{
|
||||
"text": "Production Deployment Failed - Rollback Initiated! :warning:",
|
||||
"attachments": [{
|
||||
"color": "danger",
|
||||
"fields": [
|
||||
{ "title": "Environment", "value": "production", "short": true },
|
||||
{ "title": "Failed Version", "value": "${{ needs.prepare-deployment.outputs.version }}", "short": true }
|
||||
]
|
||||
}]
|
||||
}
|
||||
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
|
||||
356
marketing-agent/.github/workflows/ci.yml
vendored
Normal file
356
marketing-agent/.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,356 @@
|
||||
name: CI Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
env:
|
||||
NODE_VERSION: '18.x'
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
# Code Quality Checks
|
||||
lint-and-format:
|
||||
name: Lint and Format Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
|
||||
- name: Check code formatting
|
||||
run: npm run format:check
|
||||
|
||||
# Security Scanning
|
||||
security-scan:
|
||||
name: Security Vulnerability Scan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run security audit
|
||||
run: npm audit --audit-level=moderate
|
||||
|
||||
- name: Run Snyk security scan
|
||||
uses: snyk/actions/node@master
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
args: --severity-threshold=high
|
||||
|
||||
# Unit Tests
|
||||
unit-tests:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
service: [api-gateway, orchestrator, scheduler, analytics, workflow]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./services/${{ matrix.service }}
|
||||
run: npm ci
|
||||
|
||||
- name: Run unit tests
|
||||
working-directory: ./services/${{ matrix.service }}
|
||||
run: npm test
|
||||
|
||||
- name: Upload coverage reports
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./services/${{ matrix.service }}/coverage/lcov.info
|
||||
flags: ${{ matrix.service }}
|
||||
name: ${{ matrix.service }}-coverage
|
||||
|
||||
# Integration Tests
|
||||
integration-tests:
|
||||
name: Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: [unit-tests]
|
||||
services:
|
||||
mongodb:
|
||||
image: mongo:6
|
||||
ports:
|
||||
- 27017:27017
|
||||
options: >-
|
||||
--health-cmd "mongosh --eval 'db.adminCommand({ping: 1})'"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
elasticsearch:
|
||||
image: elasticsearch:8.12.0
|
||||
ports:
|
||||
- 9200:9200
|
||||
env:
|
||||
discovery.type: single-node
|
||||
xpack.security.enabled: false
|
||||
options: >-
|
||||
--health-cmd "curl -f http://localhost:9200/_cluster/health"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run integration tests
|
||||
env:
|
||||
MONGODB_URI: mongodb://localhost:27017/test
|
||||
REDIS_HOST: localhost
|
||||
ELASTICSEARCH_NODE: http://localhost:9200
|
||||
run: npm run test:integration
|
||||
|
||||
# Build Docker Images
|
||||
build-images:
|
||||
name: Build Docker Images
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint-and-format, security-scan, unit-tests]
|
||||
strategy:
|
||||
matrix:
|
||||
service:
|
||||
- api-gateway
|
||||
- orchestrator
|
||||
- claude-agent
|
||||
- gramjs-adapter
|
||||
- safety-guard
|
||||
- analytics
|
||||
- compliance-guard
|
||||
- ab-testing
|
||||
- workflow
|
||||
- webhook
|
||||
- template
|
||||
- i18n
|
||||
- user-management
|
||||
- scheduler
|
||||
- logging
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ matrix.service }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha,prefix={{branch}}-
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./services/${{ matrix.service }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
BUILD_DATE=${{ github.event.head_commit.timestamp }}
|
||||
VCS_REF=${{ github.sha }}
|
||||
VERSION=${{ steps.meta.outputs.version }}
|
||||
|
||||
# Build Frontend
|
||||
build-frontend:
|
||||
name: Build Frontend
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint-and-format, security-scan]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./frontend
|
||||
run: npm ci
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: ./frontend
|
||||
run: npm run build
|
||||
|
||||
- name: Run Lighthouse CI
|
||||
uses: treosh/lighthouse-ci-action@v10
|
||||
with:
|
||||
uploadArtifacts: true
|
||||
temporaryPublicStorage: true
|
||||
runs: 3
|
||||
configPath: ./frontend/.lighthouserc.json
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frontend-build
|
||||
path: ./frontend/dist
|
||||
retention-days: 7
|
||||
|
||||
# E2E Tests
|
||||
e2e-tests:
|
||||
name: End-to-End Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: [integration-tests, build-frontend]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Download frontend build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: frontend-build
|
||||
path: ./frontend/dist
|
||||
|
||||
- name: Start services with docker-compose
|
||||
run: |
|
||||
docker-compose -f docker-compose.test.yml up -d
|
||||
./scripts/wait-for-services.sh
|
||||
|
||||
- name: Run E2E tests
|
||||
run: npm run test:e2e
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-test-results
|
||||
path: ./tests/e2e/results
|
||||
retention-days: 7
|
||||
|
||||
# Performance Tests
|
||||
performance-tests:
|
||||
name: Performance Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-images, build-frontend]
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup k6
|
||||
run: |
|
||||
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
|
||||
echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install k6
|
||||
|
||||
- name: Start services
|
||||
run: |
|
||||
docker-compose -f docker-compose.perf.yml up -d
|
||||
./scripts/wait-for-services.sh
|
||||
|
||||
- name: Run performance tests
|
||||
run: |
|
||||
k6 run ./tests/performance/load-test.js
|
||||
k6 run ./tests/performance/stress-test.js
|
||||
k6 run ./tests/performance/spike-test.js
|
||||
|
||||
- name: Upload performance results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: performance-results
|
||||
path: ./tests/performance/results
|
||||
retention-days: 30
|
||||
|
||||
# Dependency Check
|
||||
dependency-check:
|
||||
name: Dependency License Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check dependency licenses
|
||||
uses: fossa-contrib/fossa-action@v2
|
||||
with:
|
||||
api-key: ${{ secrets.FOSSA_API_KEY }}
|
||||
|
||||
# SonarQube Analysis
|
||||
sonarqube:
|
||||
name: SonarQube Analysis
|
||||
runs-on: ubuntu-latest
|
||||
needs: [unit-tests, integration-tests]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||
|
||||
# Notify on failure
|
||||
notify-failure:
|
||||
name: Notify on Failure
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint-and-format, security-scan, unit-tests, integration-tests, build-images, build-frontend, e2e-tests]
|
||||
if: failure()
|
||||
steps:
|
||||
- name: Send Slack notification
|
||||
uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
text: 'CI Pipeline Failed for ${{ github.repository }}'
|
||||
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
|
||||
channel: '#ci-notifications'
|
||||
username: 'GitHub Actions'
|
||||
icon_emoji: ':warning:'
|
||||
247
marketing-agent/.github/workflows/security.yml
vendored
Normal file
247
marketing-agent/.github/workflows/security.yml
vendored
Normal file
@@ -0,0 +1,247 @@
|
||||
name: Security Scanning
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # Daily at 2 AM UTC
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
# Container Security Scanning
|
||||
container-scan:
|
||||
name: Container Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
service:
|
||||
- api-gateway
|
||||
- orchestrator
|
||||
- claude-agent
|
||||
- analytics
|
||||
- logging
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run Trivy scanner
|
||||
uses: aquasecurity/trivy-action@master
|
||||
with:
|
||||
scan-type: 'fs'
|
||||
scan-ref: './services/${{ matrix.service }}'
|
||||
format: 'sarif'
|
||||
output: 'trivy-${{ matrix.service }}.sarif'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
|
||||
- name: Upload Trivy results to GitHub Security
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: 'trivy-${{ matrix.service }}.sarif'
|
||||
category: 'trivy-${{ matrix.service }}'
|
||||
|
||||
# SAST - Static Application Security Testing
|
||||
sast-scan:
|
||||
name: SAST Scan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: javascript, typescript
|
||||
queries: security-and-quality
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
- name: Run Semgrep
|
||||
uses: returntocorp/semgrep-action@v1
|
||||
with:
|
||||
config: >-
|
||||
p/security-audit
|
||||
p/nodejs
|
||||
p/typescript
|
||||
p/mongodb
|
||||
p/jwt
|
||||
|
||||
# Secret Scanning
|
||||
secret-scan:
|
||||
name: Secret Scanning
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: TruffleHog scan
|
||||
uses: trufflesecurity/trufflehog@main
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.repository.default_branch }}
|
||||
head: HEAD
|
||||
extra_args: --debug --only-verified
|
||||
|
||||
- name: GitLeaks scan
|
||||
uses: gitleaks/gitleaks-action@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Dependency Security Audit
|
||||
dependency-audit:
|
||||
name: Dependency Security Audit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run npm audit
|
||||
run: |
|
||||
find . -name package.json -not -path "*/node_modules/*" -execdir npm audit --audit-level=moderate \;
|
||||
|
||||
- name: Run OWASP Dependency Check
|
||||
uses: dependency-check/Dependency-Check_Action@main
|
||||
with:
|
||||
project: 'marketing-agent'
|
||||
path: '.'
|
||||
format: 'HTML'
|
||||
args: >
|
||||
--enableRetired
|
||||
--enableExperimental
|
||||
|
||||
- name: Upload dependency check results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dependency-check-report
|
||||
path: reports/
|
||||
retention-days: 30
|
||||
|
||||
# Infrastructure Security Scan
|
||||
infrastructure-scan:
|
||||
name: Infrastructure Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Checkov scan
|
||||
uses: bridgecrewio/checkov-action@master
|
||||
with:
|
||||
directory: .
|
||||
framework: all
|
||||
output_format: sarif
|
||||
output_file_path: checkov.sarif
|
||||
download_external_modules: true
|
||||
|
||||
- name: Upload Checkov results
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: checkov.sarif
|
||||
category: infrastructure
|
||||
|
||||
- name: Terraform security scan
|
||||
uses: aquasecurity/tfsec-action@v1.0.0
|
||||
with:
|
||||
working_directory: ./infrastructure/terraform
|
||||
|
||||
# API Security Testing
|
||||
api-security-test:
|
||||
name: API Security Testing
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'schedule'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup OWASP ZAP
|
||||
run: |
|
||||
docker pull owasp/zap2docker-stable
|
||||
|
||||
- name: Start application
|
||||
run: |
|
||||
docker-compose -f docker-compose.security.yml up -d
|
||||
./scripts/wait-for-services.sh
|
||||
|
||||
- name: Run ZAP API scan
|
||||
run: |
|
||||
docker run -v $(pwd):/zap/wrk/:rw \
|
||||
-t owasp/zap2docker-stable zap-api-scan.py \
|
||||
-t http://host.docker.internal:3000/api-docs.json \
|
||||
-f openapi \
|
||||
-r zap-api-report.html \
|
||||
-w zap-api-report.md
|
||||
|
||||
- name: Upload ZAP results
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: zap-api-report
|
||||
path: |
|
||||
zap-api-report.html
|
||||
zap-api-report.md
|
||||
retention-days: 30
|
||||
|
||||
# Compliance Checks
|
||||
compliance-check:
|
||||
name: Compliance Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: License compliance check
|
||||
uses: fossa-contrib/fossa-action@v2
|
||||
with:
|
||||
api-key: ${{ secrets.FOSSA_API_KEY }}
|
||||
run-tests: true
|
||||
|
||||
- name: GDPR compliance check
|
||||
run: |
|
||||
# Check for PII handling
|
||||
./scripts/compliance/gdpr-check.sh
|
||||
|
||||
- name: SOC2 compliance check
|
||||
run: |
|
||||
# Check security controls
|
||||
./scripts/compliance/soc2-check.sh
|
||||
|
||||
# Security Report Generation
|
||||
generate-report:
|
||||
name: Generate Security Report
|
||||
runs-on: ubuntu-latest
|
||||
needs: [container-scan, sast-scan, secret-scan, dependency-audit, infrastructure-scan]
|
||||
if: always()
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: security-reports/
|
||||
|
||||
- name: Generate consolidated report
|
||||
run: |
|
||||
python scripts/security/generate-security-report.py \
|
||||
--input security-reports/ \
|
||||
--output security-summary.html
|
||||
|
||||
- name: Upload security summary
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: security-summary
|
||||
path: security-summary.html
|
||||
retention-days: 90
|
||||
|
||||
- name: Create security issue if critical findings
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const report = JSON.parse(fs.readFileSync('security-summary.json', 'utf8'));
|
||||
|
||||
if (report.critical_findings > 0) {
|
||||
await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: `🚨 ${report.critical_findings} Critical Security Findings`,
|
||||
body: report.summary,
|
||||
labels: ['security', 'critical']
|
||||
});
|
||||
}
|
||||
126
marketing-agent/.github/workflows/test.yml
vendored
Normal file
126
marketing-agent/.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x, 20.x]
|
||||
mongodb-version: ['6.0', '7.0']
|
||||
redis-version: ['7']
|
||||
|
||||
services:
|
||||
mongodb:
|
||||
image: mongo:${{ matrix.mongodb-version }}
|
||||
ports:
|
||||
- 27017:27017
|
||||
options: >-
|
||||
--health-cmd mongosh
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
redis:
|
||||
image: redis:${{ matrix.redis-version }}
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm ci
|
||||
cd services/api-gateway && npm ci
|
||||
cd ../orchestrator && npm ci
|
||||
cd ../analytics && npm ci
|
||||
cd ../scheduler && npm ci
|
||||
cd ../user-management && npm ci
|
||||
cd ../..
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
|
||||
- name: Run unit tests
|
||||
env:
|
||||
NODE_ENV: test
|
||||
JWT_SECRET: test-jwt-secret
|
||||
MONGODB_URI: mongodb://localhost:27017/test
|
||||
REDIS_URL: redis://localhost:6379
|
||||
run: npm run test:unit
|
||||
|
||||
- name: Run integration tests
|
||||
env:
|
||||
NODE_ENV: test
|
||||
JWT_SECRET: test-jwt-secret
|
||||
MONGODB_URI: mongodb://localhost:27017/test
|
||||
REDIS_URL: redis://localhost:6379
|
||||
run: npm run test:integration
|
||||
|
||||
- name: Run E2E tests
|
||||
env:
|
||||
NODE_ENV: test
|
||||
JWT_SECRET: test-jwt-secret
|
||||
MONGODB_URI: mongodb://localhost:27017/test
|
||||
REDIS_URL: redis://localhost:6379
|
||||
run: npm run test:e2e
|
||||
|
||||
- name: Generate coverage report
|
||||
run: npm run test:coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./coverage/lcov.info
|
||||
flags: unittests
|
||||
name: codecov-umbrella
|
||||
fail_ci_if_error: true
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: test-results-${{ matrix.node-version }}-${{ matrix.mongodb-version }}
|
||||
path: test-results/junit.xml
|
||||
|
||||
- name: Upload coverage artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage-${{ matrix.node-version }}-${{ matrix.mongodb-version }}
|
||||
path: coverage/
|
||||
|
||||
test-summary:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
|
||||
- name: Publish test results
|
||||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||
if: always()
|
||||
with:
|
||||
files: '**/junit.xml'
|
||||
check_name: Test Results
|
||||
comment_title: Test Results Summary
|
||||
97
marketing-agent/.gitignore
vendored
Normal file
97
marketing-agent/.gitignore
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Build
|
||||
dist/
|
||||
build/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
.dockerignore
|
||||
|
||||
# Sessions
|
||||
sessions/
|
||||
*.session
|
||||
|
||||
# Uploads
|
||||
uploads/
|
||||
temp/
|
||||
|
||||
# Database
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
*.db
|
||||
|
||||
# Certificates
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
*.csr
|
||||
|
||||
# Backups
|
||||
backups/
|
||||
*.backup
|
||||
|
||||
# Cache
|
||||
.cache/
|
||||
*.cache
|
||||
|
||||
# Monitoring
|
||||
prometheus-data/
|
||||
grafana-data/
|
||||
|
||||
# Documentation
|
||||
docs/_build/
|
||||
site/
|
||||
|
||||
# Python (if using any Python scripts)
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
venv/
|
||||
env/
|
||||
|
||||
# Terraform
|
||||
*.tfstate
|
||||
*.tfstate.backup
|
||||
.terraform/
|
||||
|
||||
# Kubernetes
|
||||
kubeconfig
|
||||
*.kubeconfig
|
||||
|
||||
# Misc
|
||||
.history/
|
||||
*.orig
|
||||
.grunt
|
||||
.sass-cache
|
||||
302
marketing-agent/BILLING_SYSTEM.md
Normal file
302
marketing-agent/BILLING_SYSTEM.md
Normal file
@@ -0,0 +1,302 @@
|
||||
# 支付和计费系统
|
||||
|
||||
## 概述
|
||||
|
||||
本系统实现了完整的支付和计费功能,支持订阅管理、账单生成、支付处理和财务报表。集成了 Stripe 作为支付处理器,支持信用卡和银行账户支付。
|
||||
|
||||
## 系统架构
|
||||
|
||||
### 服务组件
|
||||
|
||||
1. **Billing Service** (`services/billing-service/`)
|
||||
- 端口:3010
|
||||
- 负责所有计费相关功能
|
||||
- 集成 Stripe API
|
||||
- 处理 Webhook 事件
|
||||
|
||||
2. **API Gateway 集成**
|
||||
- 路由前缀:`/api/v1/billing/`
|
||||
- 自动转发到 Billing Service
|
||||
- 支持认证和限流
|
||||
|
||||
3. **前端组件**
|
||||
- 计费仪表板
|
||||
- 订阅管理
|
||||
- 支付方式管理
|
||||
- 账单历史
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. 订阅管理
|
||||
|
||||
#### 订阅计划
|
||||
- **免费版**:基础功能,限制使用量
|
||||
- **入门版**:$29/月,适合小团队
|
||||
- **专业版**:$99/月,高级功能
|
||||
- **企业版**:$299/月,无限制使用
|
||||
|
||||
#### 订阅功能
|
||||
- 创建和升级订阅
|
||||
- 取消和恢复订阅
|
||||
- 试用期管理
|
||||
- 使用量跟踪
|
||||
- 优惠券和折扣
|
||||
|
||||
### 2. 支付方式
|
||||
|
||||
#### 支持的支付类型
|
||||
- 信用卡/借记卡
|
||||
- 银行账户(ACH)
|
||||
|
||||
#### 支付方式管理
|
||||
- 添加和删除支付方式
|
||||
- 设置默认支付方式
|
||||
- 支付方式验证
|
||||
- 账单地址管理
|
||||
|
||||
### 3. 账单和发票
|
||||
|
||||
#### 账单功能
|
||||
- 自动生成月度账单
|
||||
- 手动创建账单
|
||||
- 账单支付和退款
|
||||
- PDF 账单下载
|
||||
- 邮件通知
|
||||
|
||||
#### 账单状态
|
||||
- 草稿(Draft)
|
||||
- 待支付(Open)
|
||||
- 已支付(Paid)
|
||||
- 已作废(Void)
|
||||
- 无法收回(Uncollectible)
|
||||
|
||||
### 4. 交易管理
|
||||
|
||||
#### 交易类型
|
||||
- 支付(Payment)
|
||||
- 退款(Refund)
|
||||
- 调整(Adjustment)
|
||||
- 费用(Fee)
|
||||
|
||||
#### 交易功能
|
||||
- 交易历史查询
|
||||
- 交易汇总报表
|
||||
- 导出功能(CSV、PDF、Excel)
|
||||
- 退款处理
|
||||
|
||||
## 数据模型
|
||||
|
||||
### Subscription 模型
|
||||
```javascript
|
||||
{
|
||||
tenantId: ObjectId, // 租户ID
|
||||
customerId: String, // 客户ID
|
||||
plan: String, // 订阅计划
|
||||
status: String, // 状态
|
||||
currentPeriodStart: Date, // 当前周期开始
|
||||
currentPeriodEnd: Date, // 当前周期结束
|
||||
trialEnd: Date, // 试用期结束
|
||||
usage: Object, // 使用量跟踪
|
||||
metadata: Object // 元数据
|
||||
}
|
||||
```
|
||||
|
||||
### Invoice 模型
|
||||
```javascript
|
||||
{
|
||||
tenantId: ObjectId, // 租户ID
|
||||
invoiceNumber: String, // 账单号
|
||||
customerId: String, // 客户ID
|
||||
status: String, // 状态
|
||||
lineItems: Array, // 行项目
|
||||
subtotal: Number, // 小计
|
||||
tax: Object, // 税费
|
||||
total: Number, // 总计
|
||||
amountDue: Number, // 应付金额
|
||||
dueDate: Date // 到期日
|
||||
}
|
||||
```
|
||||
|
||||
### PaymentMethod 模型
|
||||
```javascript
|
||||
{
|
||||
tenantId: ObjectId, // 租户ID
|
||||
customerId: String, // 客户ID
|
||||
type: String, // 类型(card/bank_account)
|
||||
card: Object, // 卡片信息
|
||||
bankAccount: Object, // 银行账户信息
|
||||
isDefault: Boolean, // 是否默认
|
||||
status: String // 状态
|
||||
}
|
||||
```
|
||||
|
||||
### Transaction 模型
|
||||
```javascript
|
||||
{
|
||||
tenantId: ObjectId, // 租户ID
|
||||
transactionId: String, // 交易ID
|
||||
type: String, // 类型
|
||||
status: String, // 状态
|
||||
amount: Number, // 金额
|
||||
currency: String, // 货币
|
||||
fee: Number, // 手续费
|
||||
net: Number, // 净额
|
||||
processedAt: Date // 处理时间
|
||||
}
|
||||
```
|
||||
|
||||
## API 端点
|
||||
|
||||
### 订阅 API
|
||||
- `GET /api/v1/billing/subscriptions` - 获取订阅列表
|
||||
- `POST /api/v1/billing/subscriptions` - 创建订阅
|
||||
- `PATCH /api/v1/billing/subscriptions/:id` - 更新订阅
|
||||
- `POST /api/v1/billing/subscriptions/:id/cancel` - 取消订阅
|
||||
- `POST /api/v1/billing/subscriptions/:id/reactivate` - 恢复订阅
|
||||
- `POST /api/v1/billing/subscriptions/:id/usage` - 记录使用量
|
||||
|
||||
### 账单 API
|
||||
- `GET /api/v1/billing/invoices` - 获取账单列表
|
||||
- `POST /api/v1/billing/invoices` - 创建账单
|
||||
- `GET /api/v1/billing/invoices/:id` - 获取账单详情
|
||||
- `POST /api/v1/billing/invoices/:id/pay` - 支付账单
|
||||
- `GET /api/v1/billing/invoices/:id/pdf` - 下载账单 PDF
|
||||
|
||||
### 支付方式 API
|
||||
- `GET /api/v1/billing/payment-methods` - 获取支付方式列表
|
||||
- `POST /api/v1/billing/payment-methods` - 添加支付方式
|
||||
- `DELETE /api/v1/billing/payment-methods/:id` - 删除支付方式
|
||||
- `POST /api/v1/billing/payment-methods/:id/default` - 设为默认
|
||||
|
||||
### 交易 API
|
||||
- `GET /api/v1/billing/transactions` - 获取交易列表
|
||||
- `POST /api/v1/billing/transactions/:id/refund` - 创建退款
|
||||
- `GET /api/v1/billing/transactions/summary/:period` - 获取汇总
|
||||
- `GET /api/v1/billing/transactions/export/:format` - 导出交易
|
||||
|
||||
## Webhook 集成
|
||||
|
||||
### Stripe Webhook 事件
|
||||
系统监听以下 Stripe webhook 事件:
|
||||
- `customer.subscription.created` - 订阅创建
|
||||
- `customer.subscription.updated` - 订阅更新
|
||||
- `customer.subscription.deleted` - 订阅删除
|
||||
- `invoice.payment_succeeded` - 支付成功
|
||||
- `invoice.payment_failed` - 支付失败
|
||||
|
||||
### Webhook 端点
|
||||
```
|
||||
POST /api/v1/billing/webhooks/stripe
|
||||
```
|
||||
|
||||
## 安全性
|
||||
|
||||
### 支付安全
|
||||
- 使用 Stripe.js 进行安全的支付信息收集
|
||||
- 不存储完整的信用卡号或银行账户信息
|
||||
- 所有支付数据通过 HTTPS 传输
|
||||
- PCI DSS 合规
|
||||
|
||||
### 访问控制
|
||||
- 租户隔离:每个租户只能访问自己的计费数据
|
||||
- 角色权限:管理员才能访问计费设置
|
||||
- API 认证:所有端点需要 JWT 认证
|
||||
|
||||
## 配置
|
||||
|
||||
### 环境变量
|
||||
```env
|
||||
# Stripe 配置
|
||||
STRIPE_SECRET_KEY=sk_test_...
|
||||
STRIPE_PUBLISHABLE_KEY=pk_test_...
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
|
||||
# 计费服务配置
|
||||
BILLING_URL=http://localhost:3010
|
||||
BILLING_PORT=3010
|
||||
|
||||
# 数据库
|
||||
MONGODB_URI=mongodb://localhost:27017/billing
|
||||
```
|
||||
|
||||
### 计费配置
|
||||
```javascript
|
||||
{
|
||||
currency: 'USD', // 默认货币
|
||||
taxRate: 0, // 税率
|
||||
trialDays: 14, // 试用期天数
|
||||
gracePeriodDays: 7, // 宽限期天数
|
||||
dunningAttempts: 3, // 催款尝试次数
|
||||
dunningIntervalDays: 3 // 催款间隔天数
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 创建订阅
|
||||
```javascript
|
||||
const subscription = await createSubscription({
|
||||
plan: 'professional',
|
||||
billingCycle: 'monthly',
|
||||
paymentMethodId: 'pm_xxx'
|
||||
});
|
||||
```
|
||||
|
||||
### 记录使用量
|
||||
```javascript
|
||||
await recordUsage(subscriptionId, {
|
||||
metric: 'messages',
|
||||
quantity: 1000
|
||||
});
|
||||
```
|
||||
|
||||
### 创建退款
|
||||
```javascript
|
||||
const refund = await createRefund(transactionId, {
|
||||
amount: 50.00,
|
||||
reason: 'Customer request'
|
||||
});
|
||||
```
|
||||
|
||||
## 后台任务
|
||||
|
||||
系统运行以下定时任务:
|
||||
- **检查即将到期的订阅**:每日运行,发送续订提醒
|
||||
- **检查逾期账单**:每6小时运行,发送催款通知
|
||||
- **检查即将到期的支付方式**:每日运行,提醒更新
|
||||
- **处理失败的交易**:每小时运行,重试失败的支付
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **支付失败**
|
||||
- 检查支付方式是否有效
|
||||
- 确认账户余额充足
|
||||
- 查看 Stripe 日志获取详细错误
|
||||
|
||||
2. **Webhook 失败**
|
||||
- 验证 webhook 签名密钥
|
||||
- 检查网络连接
|
||||
- 查看 webhook 日志
|
||||
|
||||
3. **订阅状态不同步**
|
||||
- 手动同步 Stripe 数据
|
||||
- 检查 webhook 事件处理
|
||||
- 验证数据库连接
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **定期对账**:每月对比 Stripe 和本地数据
|
||||
2. **监控失败率**:跟踪支付失败和退款率
|
||||
3. **优化重试策略**:根据失败原因调整重试时间
|
||||
4. **保持合规**:遵守 PCI DSS 和数据保护法规
|
||||
5. **文档维护**:及时更新账单模板和条款
|
||||
|
||||
## 未来改进
|
||||
|
||||
1. **多币种支持**:支持更多货币类型
|
||||
2. **更多支付方式**:支持 PayPal、支付宝等
|
||||
3. **发票定制**:自定义发票模板和品牌
|
||||
4. **高级报表**:收入预测和财务分析
|
||||
5. **自动化催收**:智能催款流程
|
||||
495
marketing-agent/DEPLOYMENT.md
Normal file
495
marketing-agent/DEPLOYMENT.md
Normal file
@@ -0,0 +1,495 @@
|
||||
# Telegram Marketing Agent System - Deployment Guide
|
||||
|
||||
This guide provides comprehensive instructions for deploying the Telegram Marketing Agent System in various environments.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Prerequisites](#prerequisites)
|
||||
2. [Environment Setup](#environment-setup)
|
||||
3. [Local Development](#local-development)
|
||||
4. [Docker Deployment](#docker-deployment)
|
||||
5. [Kubernetes Deployment](#kubernetes-deployment)
|
||||
6. [Production Deployment](#production-deployment)
|
||||
7. [Monitoring & Maintenance](#monitoring--maintenance)
|
||||
8. [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### System Requirements
|
||||
|
||||
- **OS**: Linux (Ubuntu 20.04+ recommended), macOS, or Windows with WSL2
|
||||
- **CPU**: 4+ cores recommended
|
||||
- **RAM**: 16GB minimum, 32GB recommended
|
||||
- **Storage**: 50GB+ free space
|
||||
- **Network**: Stable internet connection with open ports
|
||||
|
||||
### Software Requirements
|
||||
|
||||
- Docker 20.10+ and Docker Compose 2.0+
|
||||
- Node.js 18+ and npm 8+
|
||||
- Git
|
||||
- MongoDB 5.0+
|
||||
- PostgreSQL 14+
|
||||
- Redis 7.0+
|
||||
- RabbitMQ 3.9+
|
||||
- Elasticsearch 8.0+ (optional)
|
||||
- ClickHouse (optional)
|
||||
|
||||
### API Keys Required
|
||||
|
||||
1. **Anthropic API Key** - For Claude AI integration
|
||||
2. **OpenAI API Key** - For content moderation
|
||||
3. **Google Cloud Project** - For additional NLP services
|
||||
4. **Telegram API Credentials** - API ID and Hash
|
||||
|
||||
## Environment Setup
|
||||
|
||||
### 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-org/telegram-marketing-agent.git
|
||||
cd telegram-marketing-agent/marketing-agent
|
||||
```
|
||||
|
||||
### 2. Create Environment File
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env` and add your API keys and configuration:
|
||||
|
||||
```env
|
||||
# Required API Keys
|
||||
ANTHROPIC_API_KEY=your_anthropic_api_key
|
||||
OPENAI_API_KEY=your_openai_api_key
|
||||
GOOGLE_CLOUD_PROJECT=your_project_id
|
||||
|
||||
# JWT Secret (generate a secure random string)
|
||||
JWT_SECRET=your-super-secret-key-min-32-chars
|
||||
|
||||
# Telegram Configuration
|
||||
TELEGRAM_API_ID=your_telegram_api_id
|
||||
TELEGRAM_API_HASH=your_telegram_api_hash
|
||||
|
||||
# Update other configurations as needed
|
||||
```
|
||||
|
||||
### 3. Generate Secure Keys
|
||||
|
||||
```bash
|
||||
# Generate JWT Secret
|
||||
openssl rand -base64 32
|
||||
|
||||
# Generate Encryption Key
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
# Install dependencies for all services
|
||||
for service in services/*; do
|
||||
if [ -d "$service" ]; then
|
||||
echo "Installing dependencies for $service"
|
||||
cd "$service"
|
||||
npm install
|
||||
cd ../..
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### 2. Start Infrastructure Services
|
||||
|
||||
```bash
|
||||
# Start databases and message brokers
|
||||
docker-compose up -d postgres mongodb redis rabbitmq elasticsearch
|
||||
```
|
||||
|
||||
### 3. Run Database Migrations
|
||||
|
||||
```bash
|
||||
# MongoDB indexes
|
||||
docker exec -it marketing_mongodb mongosh marketing_agent --eval '
|
||||
db.tasks.createIndex({ taskId: 1 }, { unique: true });
|
||||
db.campaigns.createIndex({ campaignId: 1 }, { unique: true });
|
||||
db.sessions.createIndex({ sessionId: 1 }, { unique: true });
|
||||
db.sessions.createIndex({ updatedAt: 1 }, { expireAfterSeconds: 2592000 });
|
||||
'
|
||||
```
|
||||
|
||||
### 4. Start Services Individually
|
||||
|
||||
```bash
|
||||
# Terminal 1: API Gateway
|
||||
cd services/api-gateway
|
||||
npm run dev
|
||||
|
||||
# Terminal 2: Orchestrator
|
||||
cd services/orchestrator
|
||||
npm run dev
|
||||
|
||||
# Terminal 3: Claude Agent
|
||||
cd services/claude-agent
|
||||
npm run dev
|
||||
|
||||
# Continue for other services...
|
||||
```
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
### 1. Build All Services
|
||||
|
||||
```bash
|
||||
# Build all Docker images
|
||||
docker-compose build
|
||||
```
|
||||
|
||||
### 2. Start All Services
|
||||
|
||||
```bash
|
||||
# Start all services
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Check service health
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### 3. Initialize Data
|
||||
|
||||
```bash
|
||||
# Create admin user
|
||||
curl -X POST http://localhost:3000/api/v1/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "admin",
|
||||
"password": "secure_password",
|
||||
"email": "admin@example.com"
|
||||
}'
|
||||
```
|
||||
|
||||
### 4. Access Services
|
||||
|
||||
- **API Gateway**: http://localhost:3000
|
||||
- **API Documentation**: http://localhost:3000/api-docs
|
||||
- **RabbitMQ Management**: http://localhost:15672 (admin/admin)
|
||||
- **Grafana**: http://localhost:3001 (admin/admin)
|
||||
- **Prometheus**: http://localhost:9090
|
||||
|
||||
## Kubernetes Deployment
|
||||
|
||||
### 1. Create Namespace
|
||||
|
||||
```bash
|
||||
kubectl create namespace marketing-agent
|
||||
```
|
||||
|
||||
### 2. Create Secrets
|
||||
|
||||
```bash
|
||||
# Create secret for API keys
|
||||
kubectl create secret generic api-keys \
|
||||
--from-literal=anthropic-api-key=$ANTHROPIC_API_KEY \
|
||||
--from-literal=openai-api-key=$OPENAI_API_KEY \
|
||||
--from-literal=jwt-secret=$JWT_SECRET \
|
||||
-n marketing-agent
|
||||
```
|
||||
|
||||
### 3. Apply Configurations
|
||||
|
||||
```bash
|
||||
# Apply all Kubernetes manifests
|
||||
kubectl apply -f infrastructure/kubernetes/ -n marketing-agent
|
||||
|
||||
# Check deployment status
|
||||
kubectl get pods -n marketing-agent
|
||||
kubectl get services -n marketing-agent
|
||||
```
|
||||
|
||||
### 4. Setup Ingress
|
||||
|
||||
```bash
|
||||
# Apply ingress configuration
|
||||
kubectl apply -f infrastructure/kubernetes/ingress.yaml -n marketing-agent
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### 1. Security Hardening
|
||||
|
||||
#### SSL/TLS Configuration
|
||||
|
||||
```nginx
|
||||
# nginx/conf.d/ssl.conf
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name api.yourdomain.com;
|
||||
|
||||
ssl_certificate /etc/ssl/certs/your-cert.pem;
|
||||
ssl_certificate_key /etc/ssl/private/your-key.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
proxy_pass http://api-gateway:3000;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
```bash
|
||||
# Production .env
|
||||
NODE_ENV=production
|
||||
LOG_LEVEL=warn
|
||||
DEBUG=false
|
||||
|
||||
# Use strong passwords
|
||||
POSTGRES_PASSWORD=$(openssl rand -base64 32)
|
||||
RABBITMQ_DEFAULT_PASS=$(openssl rand -base64 32)
|
||||
```
|
||||
|
||||
### 2. Database Setup
|
||||
|
||||
#### PostgreSQL
|
||||
|
||||
```sql
|
||||
-- Create production database
|
||||
CREATE DATABASE marketing_agent_prod;
|
||||
CREATE USER marketing_prod WITH ENCRYPTED PASSWORD 'strong_password';
|
||||
GRANT ALL PRIVILEGES ON DATABASE marketing_agent_prod TO marketing_prod;
|
||||
|
||||
-- Enable extensions
|
||||
\c marketing_agent_prod
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
```
|
||||
|
||||
#### MongoDB
|
||||
|
||||
```javascript
|
||||
// Create production user
|
||||
use marketing_agent_prod
|
||||
db.createUser({
|
||||
user: "marketing_prod",
|
||||
pwd: "strong_password",
|
||||
roles: [
|
||||
{ role: "readWrite", db: "marketing_agent_prod" }
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Scaling Configuration
|
||||
|
||||
#### Docker Swarm
|
||||
|
||||
```bash
|
||||
# Initialize swarm
|
||||
docker swarm init
|
||||
|
||||
# Deploy stack
|
||||
docker stack deploy -c docker-compose.prod.yml marketing-agent
|
||||
|
||||
# Scale services
|
||||
docker service scale marketing-agent_api-gateway=3
|
||||
docker service scale marketing-agent_orchestrator=2
|
||||
```
|
||||
|
||||
#### Kubernetes HPA
|
||||
|
||||
```yaml
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: api-gateway-hpa
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: api-gateway
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 70
|
||||
```
|
||||
|
||||
### 4. Backup Strategy
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup.sh
|
||||
|
||||
# Backup MongoDB
|
||||
docker exec marketing_mongodb mongodump \
|
||||
--uri="mongodb://localhost:27017/marketing_agent" \
|
||||
--out=/backup/mongodb-$(date +%Y%m%d)
|
||||
|
||||
# Backup PostgreSQL
|
||||
docker exec marketing_postgres pg_dump \
|
||||
-U marketing_user marketing_agent \
|
||||
> /backup/postgres-$(date +%Y%m%d).sql
|
||||
|
||||
# Backup Redis
|
||||
docker exec marketing_redis redis-cli BGSAVE
|
||||
|
||||
# Upload to S3
|
||||
aws s3 sync /backup s3://your-backup-bucket/$(date +%Y%m%d)/
|
||||
```
|
||||
|
||||
## Monitoring & Maintenance
|
||||
|
||||
### 1. Health Checks
|
||||
|
||||
```bash
|
||||
# Check all services health
|
||||
curl http://localhost:3000/health/services
|
||||
|
||||
# Individual service health
|
||||
curl http://localhost:3001/health # Orchestrator
|
||||
curl http://localhost:3002/health # Claude Agent
|
||||
```
|
||||
|
||||
### 2. Prometheus Alerts
|
||||
|
||||
```yaml
|
||||
# prometheus/alerts.yml
|
||||
groups:
|
||||
- name: marketing-agent
|
||||
rules:
|
||||
- alert: ServiceDown
|
||||
expr: up{job="api-gateway"} == 0
|
||||
for: 5m
|
||||
annotations:
|
||||
summary: "API Gateway is down"
|
||||
|
||||
- alert: HighErrorRate
|
||||
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
|
||||
for: 5m
|
||||
annotations:
|
||||
summary: "High error rate detected"
|
||||
```
|
||||
|
||||
### 3. Log Management
|
||||
|
||||
```bash
|
||||
# View logs
|
||||
docker-compose logs -f api-gateway
|
||||
|
||||
# Export logs
|
||||
docker logs marketing_api_gateway > api-gateway.log
|
||||
|
||||
# Log rotation
|
||||
cat > /etc/logrotate.d/marketing-agent << EOF
|
||||
/var/log/marketing-agent/*.log {
|
||||
daily
|
||||
rotate 14
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
### 4. Performance Tuning
|
||||
|
||||
```javascript
|
||||
// Redis optimization
|
||||
// redis.conf
|
||||
maxmemory 2gb
|
||||
maxmemory-policy allkeys-lru
|
||||
save 900 1
|
||||
save 300 10
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. Service Connection Errors
|
||||
|
||||
```bash
|
||||
# Check network connectivity
|
||||
docker network ls
|
||||
docker network inspect marketing-agent_marketing_network
|
||||
|
||||
# Restart services
|
||||
docker-compose restart api-gateway
|
||||
```
|
||||
|
||||
#### 2. Database Connection Issues
|
||||
|
||||
```bash
|
||||
# Test MongoDB connection
|
||||
docker exec -it marketing_mongodb mongosh --eval "db.adminCommand('ping')"
|
||||
|
||||
# Test PostgreSQL connection
|
||||
docker exec -it marketing_postgres psql -U marketing_user -d marketing_agent -c "SELECT 1"
|
||||
```
|
||||
|
||||
#### 3. Memory Issues
|
||||
|
||||
```bash
|
||||
# Check memory usage
|
||||
docker stats
|
||||
|
||||
# Increase memory limits in docker-compose.yml
|
||||
services:
|
||||
claude-agent:
|
||||
mem_limit: 2g
|
||||
memswap_limit: 2g
|
||||
```
|
||||
|
||||
#### 4. API Rate Limiting
|
||||
|
||||
```javascript
|
||||
// Adjust rate limits in config
|
||||
rateLimiting: {
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 200 // Increase limit
|
||||
}
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
```bash
|
||||
# Enable debug logging
|
||||
export DEBUG=true
|
||||
export LOG_LEVEL=debug
|
||||
|
||||
# Run with verbose output
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
### Support
|
||||
|
||||
For additional support:
|
||||
- Check logs in `/logs` directory
|
||||
- Review error messages in Grafana dashboards
|
||||
- Contact support team with service logs and error details
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] Change all default passwords
|
||||
- [ ] Enable SSL/TLS for all external endpoints
|
||||
- [ ] Configure firewall rules
|
||||
- [ ] Enable audit logging
|
||||
- [ ] Set up backup automation
|
||||
- [ ] Configure monitoring alerts
|
||||
- [ ] Review and update dependencies regularly
|
||||
- [ ] Implement rate limiting
|
||||
- [ ] Enable CORS properly
|
||||
- [ ] Rotate API keys periodically
|
||||
260
marketing-agent/MULTI_TENANT.md
Normal file
260
marketing-agent/MULTI_TENANT.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# Multi-Tenant Architecture
|
||||
|
||||
This document describes the multi-tenant implementation for the Telegram Marketing Intelligence Agent System.
|
||||
|
||||
## Overview
|
||||
|
||||
The system supports full multi-tenant isolation, allowing multiple organizations to use the same instance while keeping their data completely separated.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Tenant Model
|
||||
|
||||
Each tenant has:
|
||||
- **Basic Information**: Name, slug, domain
|
||||
- **Plan & Limits**: Subscription plan with resource limits
|
||||
- **Usage Tracking**: Real-time usage monitoring
|
||||
- **Billing**: Integrated billing and payment tracking
|
||||
- **Settings**: Customizable preferences per tenant
|
||||
- **Branding**: White-label support for enterprise plans
|
||||
- **Features**: Plan-based feature toggles
|
||||
- **Compliance**: GDPR and data retention settings
|
||||
|
||||
### Data Isolation
|
||||
|
||||
All data models include a `tenantId` field that ensures complete data isolation:
|
||||
- MongoDB models use compound indexes with tenantId
|
||||
- Sequelize models use foreign key references
|
||||
- All queries automatically filter by current tenant
|
||||
|
||||
### Tenant Context
|
||||
|
||||
The tenant context is determined through multiple methods:
|
||||
1. **Subdomain**: `acme.app.com` → tenant: acme
|
||||
2. **Custom Domain**: `app.acme.com` → tenant: acme
|
||||
3. **Header**: `X-Tenant-Id` for API access
|
||||
4. **URL Parameter**: `?tenant=acme` for multi-tenant admin
|
||||
5. **User Association**: From authenticated user's tenant
|
||||
|
||||
## Implementation
|
||||
|
||||
### 1. Tenant Middleware
|
||||
|
||||
```javascript
|
||||
// Automatically adds tenant context to all requests
|
||||
app.use(tenantMiddleware);
|
||||
app.use(allowCrossTenant); // For superadmin access
|
||||
```
|
||||
|
||||
### 2. Model Updates
|
||||
|
||||
All models now include tenantId:
|
||||
```javascript
|
||||
const schema = new Schema({
|
||||
tenantId: {
|
||||
type: ObjectId,
|
||||
ref: 'Tenant',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
// ... other fields
|
||||
});
|
||||
```
|
||||
|
||||
### 3. API Routes
|
||||
|
||||
#### Public Routes
|
||||
- `POST /api/v1/tenants/signup` - Create new tenant
|
||||
|
||||
#### Tenant Routes (Authenticated)
|
||||
- `GET /api/v1/tenants/current` - Get current tenant
|
||||
- `PATCH /api/v1/tenants/current/settings` - Update settings
|
||||
- `PATCH /api/v1/tenants/current/branding` - Update branding
|
||||
- `GET /api/v1/tenants/current/usage` - Get usage statistics
|
||||
|
||||
#### Superadmin Routes
|
||||
- `GET /api/v1/tenants` - List all tenants
|
||||
- `PATCH /api/v1/tenants/:id` - Update any tenant
|
||||
- `DELETE /api/v1/tenants/:id` - Delete tenant
|
||||
|
||||
### 4. Frontend Components
|
||||
|
||||
#### Tenant Settings
|
||||
- Location: `/dashboard/tenant/settings`
|
||||
- Features:
|
||||
- General settings (timezone, language, currency)
|
||||
- Security settings (2FA, SSO)
|
||||
- Branding (logo, colors, custom CSS)
|
||||
- Usage monitoring and limits
|
||||
- Compliance configuration
|
||||
|
||||
#### Tenant Management (Superadmin)
|
||||
- Location: `/dashboard/tenants`
|
||||
- Features:
|
||||
- List all tenants with filtering
|
||||
- Edit tenant plans and limits
|
||||
- Suspend/activate tenants
|
||||
- Login as tenant
|
||||
- Delete tenants
|
||||
|
||||
## Plans & Features
|
||||
|
||||
### Free Plan
|
||||
- 5 users
|
||||
- 10 campaigns
|
||||
- 1,000 messages/month
|
||||
- Basic analytics
|
||||
- Community support
|
||||
|
||||
### Starter Plan ($29/month)
|
||||
- 20 users
|
||||
- 50 campaigns
|
||||
- 10,000 messages/month
|
||||
- Automation
|
||||
- API access
|
||||
- Email support
|
||||
|
||||
### Professional Plan ($99/month)
|
||||
- 100 users
|
||||
- 200 campaigns
|
||||
- 50,000 messages/month
|
||||
- A/B testing
|
||||
- Custom reports
|
||||
- Multi-language
|
||||
- Priority support
|
||||
|
||||
### Enterprise Plan (Custom)
|
||||
- Unlimited everything
|
||||
- White-label branding
|
||||
- AI suggestions
|
||||
- SLA guarantee
|
||||
- Dedicated support
|
||||
|
||||
## Resource Limits
|
||||
|
||||
Each tenant has configurable limits:
|
||||
- `users`: Maximum number of users
|
||||
- `campaigns`: Maximum number of campaigns
|
||||
- `messagesPerMonth`: Monthly message limit
|
||||
- `telegramAccounts`: Number of Telegram accounts
|
||||
- `storage`: Storage quota in bytes
|
||||
- `apiCallsPerHour`: API rate limiting
|
||||
- `webhooks`: Number of webhooks
|
||||
- `customIntegrations`: Enable/disable custom integrations
|
||||
|
||||
## Usage Tracking
|
||||
|
||||
The system tracks usage in real-time:
|
||||
```javascript
|
||||
// Check limit before operation
|
||||
if (!tenant.checkLimit('campaigns')) {
|
||||
throw new Error('Campaign limit exceeded');
|
||||
}
|
||||
|
||||
// Increment usage after successful operation
|
||||
await tenant.incrementUsage('campaigns');
|
||||
|
||||
// Monthly reset for message limits
|
||||
await tenant.resetMonthlyUsage();
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Tenant Isolation
|
||||
- All database queries include tenant filtering
|
||||
- Compound indexes ensure query performance
|
||||
- Cross-tenant access blocked except for superadmin
|
||||
|
||||
### Authentication
|
||||
- Users belong to a single tenant
|
||||
- Tenant context included in JWT tokens
|
||||
- API keys scoped to tenant
|
||||
|
||||
### Compliance
|
||||
- Per-tenant GDPR settings
|
||||
- Configurable data retention
|
||||
- IP whitelisting support
|
||||
- Audit logging per tenant
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### For Existing Installations
|
||||
|
||||
1. **Backup your data** before migration
|
||||
|
||||
2. **Run migration script**:
|
||||
```bash
|
||||
node scripts/migrate-to-multitenant.js
|
||||
```
|
||||
|
||||
3. **Update environment variables**:
|
||||
```env
|
||||
DEFAULT_TENANT_SLUG=default
|
||||
ENABLE_MULTI_TENANT=true
|
||||
```
|
||||
|
||||
4. **Test tenant isolation**:
|
||||
```bash
|
||||
npm run test:multitenant
|
||||
```
|
||||
|
||||
### For New Installations
|
||||
|
||||
Multi-tenancy is enabled by default. The first user signup creates a new tenant automatically.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Development
|
||||
1. Always include tenantId in queries
|
||||
2. Use tenant middleware for automatic filtering
|
||||
3. Test with multiple tenants in development
|
||||
4. Validate tenant limits before operations
|
||||
|
||||
### Production
|
||||
1. Monitor tenant usage regularly
|
||||
2. Set up alerts for limit approaching
|
||||
3. Implement proper backup strategies per tenant
|
||||
4. Use custom domains for enterprise tenants
|
||||
|
||||
### Performance
|
||||
1. Ensure all queries use tenant indexes
|
||||
2. Monitor slow queries per tenant
|
||||
3. Implement caching strategies
|
||||
4. Use read replicas for analytics
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"Tenant not found" error**
|
||||
- Check tenant slug/domain configuration
|
||||
- Verify tenant status is active
|
||||
- Check subdomain DNS settings
|
||||
|
||||
2. **Data visible across tenants**
|
||||
- Ensure tenantId is in all queries
|
||||
- Check model indexes
|
||||
- Verify middleware is applied
|
||||
|
||||
3. **Performance degradation**
|
||||
- Check compound index usage
|
||||
- Monitor per-tenant query patterns
|
||||
- Consider sharding large tenants
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable tenant debug logging:
|
||||
```javascript
|
||||
// In development
|
||||
process.env.TENANT_DEBUG = 'true';
|
||||
```
|
||||
|
||||
This logs all tenant resolution and filtering operations.
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Tenant Sharding**: Distribute large tenants across databases
|
||||
2. **Tenant Templates**: Pre-configured tenant setups
|
||||
3. **Tenant Marketplace**: Share templates/workflows between tenants
|
||||
4. **Advanced Analytics**: Cross-tenant analytics for superadmin
|
||||
5. **Automated Scaling**: Dynamic resource allocation based on usage
|
||||
141
marketing-agent/QUICKSTART.md
Normal file
141
marketing-agent/QUICKSTART.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Quick Start Guide - Telegram Marketing Agent System
|
||||
|
||||
## Prerequisites
|
||||
- Docker and Docker Compose installed
|
||||
- At least 8GB of RAM available
|
||||
- Telegram API credentials (API ID and API Hash)
|
||||
|
||||
## 🚀 Quick Setup
|
||||
|
||||
### 1. Clone and Navigate
|
||||
```bash
|
||||
cd telegram-management-system/marketing-agent
|
||||
```
|
||||
|
||||
### 2. Configure Environment
|
||||
```bash
|
||||
# Copy the development environment file
|
||||
cp .env.development .env
|
||||
|
||||
# Edit the .env file and add your Telegram credentials:
|
||||
# TELEGRAM_API_ID=your-api-id
|
||||
# TELEGRAM_API_HASH=your-api-hash
|
||||
```
|
||||
|
||||
### 3. Start All Services
|
||||
```bash
|
||||
# Make scripts executable
|
||||
chmod +x scripts/*.sh
|
||||
|
||||
# Start all services
|
||||
./scripts/start-services.sh
|
||||
```
|
||||
|
||||
### 4. Verify System Health
|
||||
```bash
|
||||
./scripts/health-check.sh
|
||||
```
|
||||
|
||||
### 5. Access the Application
|
||||
- **Frontend**: http://localhost:3008
|
||||
- **API Gateway**: http://localhost:3030
|
||||
- **API Documentation**: http://localhost:3030/api-docs
|
||||
|
||||
## 📝 Default Credentials
|
||||
- **Username**: admin@example.com
|
||||
- **Password**: admin123
|
||||
|
||||
## 🔧 Service Ports
|
||||
|
||||
| Service | Port | Description |
|
||||
|---------|------|-------------|
|
||||
| Frontend | 3008 | Vue.js web interface |
|
||||
| API Gateway | 3030 | Unified API endpoint |
|
||||
| Orchestrator | 3001 | Campaign management |
|
||||
| Claude Agent | 3002 | AI integration |
|
||||
| GramJS Adapter | 3003 | Telegram integration |
|
||||
| Safety Guard | 3004 | Content moderation |
|
||||
| Analytics | 3005 | Data analytics |
|
||||
| Compliance | 3006 | Compliance management |
|
||||
| A/B Testing | 3007 | Experiment management |
|
||||
|
||||
## 🛠 Common Commands
|
||||
|
||||
### View Logs
|
||||
```bash
|
||||
# All services
|
||||
docker-compose logs -f
|
||||
|
||||
# Specific service
|
||||
docker-compose logs -f api-gateway
|
||||
```
|
||||
|
||||
### Restart Services
|
||||
```bash
|
||||
# All services
|
||||
docker-compose restart
|
||||
|
||||
# Specific service
|
||||
docker-compose restart orchestrator
|
||||
```
|
||||
|
||||
### Stop Services
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Clean Reset
|
||||
```bash
|
||||
# Stop services and remove volumes
|
||||
docker-compose down -v
|
||||
|
||||
# Start fresh
|
||||
./scripts/start-services.sh
|
||||
```
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Service Won't Start
|
||||
1. Check port conflicts: `lsof -i :PORT`
|
||||
2. View logs: `docker-compose logs SERVICE_NAME`
|
||||
3. Ensure Docker has enough resources
|
||||
|
||||
### API Gateway Connection Issues
|
||||
1. Verify API Gateway is running: `curl http://localhost:3030/health`
|
||||
2. Check service discovery: `curl http://localhost:3030/health/services`
|
||||
3. Review proxy configuration in `services/api-gateway/src/routes/proxy.js`
|
||||
|
||||
### Frontend Can't Connect to API
|
||||
1. Check browser console for errors
|
||||
2. Verify API Gateway is accessible
|
||||
3. Clear browser cache and cookies
|
||||
|
||||
### Telegram Connection Issues
|
||||
1. Ensure API credentials are correct in `.env`
|
||||
2. Check gramjs-adapter logs: `docker-compose logs gramjs-adapter`
|
||||
3. Delete session files and reconnect
|
||||
|
||||
## 📚 Next Steps
|
||||
|
||||
1. **Connect Telegram Account**: Navigate to Settings > Accounts in the web interface
|
||||
2. **Create First Campaign**: Go to Campaigns > Create Campaign
|
||||
3. **Configure AI Assistant**: Set up Claude API key in Settings
|
||||
4. **Import Contacts**: Use the data import feature in Settings
|
||||
|
||||
## 🔐 Security Notes
|
||||
|
||||
- Change default credentials immediately
|
||||
- Update JWT_SECRET in production
|
||||
- Configure proper CORS origins
|
||||
- Enable HTTPS for production deployment
|
||||
- Regularly update dependencies
|
||||
|
||||
## 📞 Support
|
||||
|
||||
- Check logs first: `docker-compose logs -f`
|
||||
- Review documentation in `/docs` directory
|
||||
- Create issues in the project repository
|
||||
|
||||
---
|
||||
|
||||
**Happy Marketing! 🚀**
|
||||
231
marketing-agent/README.md
Normal file
231
marketing-agent/README.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# Claude Code × Telegram (gramJS) Marketing Intelligence Agent
|
||||
|
||||
## 🚀 Overview
|
||||
|
||||
A comprehensive marketing intelligence system that combines Claude AI with Telegram automation for intelligent campaign management. The system provides full-chain automation from goal setting to strategy execution, with human-in-the-loop controls for high-risk decisions.
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **Orchestrator Service**
|
||||
- Task scheduling and state management
|
||||
- Redis-based task queue
|
||||
- State machine workflow engine
|
||||
- Task dependency management
|
||||
|
||||
2. **Claude Agent**
|
||||
- Function calling integration
|
||||
- Prompt template management
|
||||
- Context and conversation history
|
||||
- Multi-model support
|
||||
|
||||
3. **gramJS Adapter**
|
||||
- Telegram API integration
|
||||
- Account pool management
|
||||
- Message automation tools
|
||||
- Event handling
|
||||
|
||||
4. **Safety Guard**
|
||||
- Rate limiting and compliance
|
||||
- Content filtering
|
||||
- Account health monitoring
|
||||
- Telegram ToS compliance
|
||||
|
||||
5. **Analytics Service**
|
||||
- Real-time event tracking
|
||||
- Funnel analysis
|
||||
- User segmentation
|
||||
- ROI calculation
|
||||
|
||||
6. **A/B Testing Engine**
|
||||
- Multi-armed bandit
|
||||
- Bayesian optimization
|
||||
- Dynamic traffic allocation
|
||||
- Experiment management
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
### Backend
|
||||
- **Language**: Node.js (ES6+)
|
||||
- **Framework**: Hapi.js
|
||||
- **Database**: PostgreSQL (main), MongoDB (events), Redis (cache/queue)
|
||||
- **Message Queue**: RabbitMQ
|
||||
- **Search**: Elasticsearch
|
||||
|
||||
### Infrastructure
|
||||
- **Container**: Docker
|
||||
- **Orchestration**: Kubernetes
|
||||
- **Monitoring**: Prometheus + Grafana
|
||||
- **Tracing**: OpenTelemetry
|
||||
- **API Gateway**: Kong/Nginx
|
||||
|
||||
### AI/ML
|
||||
- **LLM**: Claude API
|
||||
- **Vector DB**: Pinecone/Weaviate
|
||||
- **ML Framework**: TensorFlow.js
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
marketing-agent/
|
||||
├── services/
|
||||
│ ├── orchestrator/ # Task orchestration service
|
||||
│ ├── claude-agent/ # Claude AI integration
|
||||
│ ├── gramjs-adapter/ # Telegram automation
|
||||
│ ├── safety-guard/ # Compliance and safety
|
||||
│ ├── analytics/ # Analytics engine
|
||||
│ ├── ab-testing/ # A/B testing service
|
||||
│ └── api-gateway/ # API gateway
|
||||
├── shared/
|
||||
│ ├── models/ # Shared data models
|
||||
│ ├── utils/ # Common utilities
|
||||
│ ├── config/ # Configuration
|
||||
│ └── types/ # TypeScript types
|
||||
├── infrastructure/
|
||||
│ ├── docker/ # Docker configs
|
||||
│ ├── k8s/ # Kubernetes manifests
|
||||
│ ├── terraform/ # Infrastructure as code
|
||||
│ └── scripts/ # Deployment scripts
|
||||
└── docs/
|
||||
├── api/ # API documentation
|
||||
├── architecture/ # Architecture diagrams
|
||||
└── guides/ # User guides
|
||||
```
|
||||
|
||||
## 🚦 Getting Started
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 18+
|
||||
- Docker & Docker Compose
|
||||
- PostgreSQL 14+
|
||||
- Redis 7+
|
||||
- MongoDB 5+
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone <repository-url>
|
||||
cd marketing-agent
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Setup environment
|
||||
cp .env.example .env
|
||||
|
||||
# Start services
|
||||
docker-compose up -d
|
||||
|
||||
# Run migrations
|
||||
npm run migrate
|
||||
|
||||
# Start development
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Environment Variables
|
||||
```env
|
||||
# Claude API
|
||||
CLAUDE_API_KEY=your_api_key
|
||||
CLAUDE_MODEL=claude-3-opus-20240229
|
||||
|
||||
# Database
|
||||
POSTGRES_URL=postgresql://user:pass@localhost:5432/marketing
|
||||
MONGODB_URL=mongodb://localhost:27017/events
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# Telegram
|
||||
TELEGRAM_API_ID=your_api_id
|
||||
TELEGRAM_API_HASH=your_api_hash
|
||||
|
||||
# Services
|
||||
RABBITMQ_URL=amqp://localhost:5672
|
||||
ELASTICSEARCH_URL=http://localhost:9200
|
||||
```
|
||||
|
||||
## 📋 Features
|
||||
|
||||
### Campaign Management
|
||||
- Goal-driven campaign creation
|
||||
- Multi-channel orchestration
|
||||
- Budget management
|
||||
- Performance tracking
|
||||
|
||||
### AI-Powered Intelligence
|
||||
- Strategy generation
|
||||
- Content optimization
|
||||
- Audience targeting
|
||||
- Predictive analytics
|
||||
|
||||
### Safety & Compliance
|
||||
- GDPR/CCPA compliance
|
||||
- Rate limiting
|
||||
- Content moderation
|
||||
- Audit logging
|
||||
|
||||
### Human-in-the-Loop
|
||||
- Approval workflows
|
||||
- Risk assessment
|
||||
- Manual override
|
||||
- Quality control
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
```bash
|
||||
# Unit tests
|
||||
npm run test
|
||||
|
||||
# Integration tests
|
||||
npm run test:integration
|
||||
|
||||
# E2E tests
|
||||
npm run test:e2e
|
||||
|
||||
# Coverage report
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
## 📊 Monitoring
|
||||
|
||||
- **Metrics**: Prometheus endpoints at `/metrics`
|
||||
- **Health**: Health checks at `/health`
|
||||
- **Logs**: Centralized logging via ELK stack
|
||||
- **Tracing**: Distributed tracing with Jaeger
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### Development
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Production
|
||||
```bash
|
||||
# Build containers
|
||||
npm run build:docker
|
||||
|
||||
# Deploy to Kubernetes
|
||||
npm run deploy:k8s
|
||||
|
||||
# Run migrations
|
||||
npm run migrate:prod
|
||||
```
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
- [API Reference](docs/api/README.md)
|
||||
- [Architecture Guide](docs/architecture/README.md)
|
||||
- [User Manual](docs/guides/user-manual.md)
|
||||
- [Developer Guide](docs/guides/developer.md)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
52
marketing-agent/diagnose-frontend.js
Normal file
52
marketing-agent/diagnose-frontend.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import axios from 'axios';
|
||||
|
||||
async function diagnose() {
|
||||
console.log('=== Frontend Diagnosis ===\n');
|
||||
|
||||
// Test 1: Frontend server
|
||||
try {
|
||||
const response = await axios.get('http://localhost:3008/');
|
||||
console.log('✅ Frontend server is running on port 3008');
|
||||
} catch (error) {
|
||||
console.log('❌ Frontend server not responding:', error.message);
|
||||
}
|
||||
|
||||
// Test 2: API proxy
|
||||
try {
|
||||
const response = await axios.get('http://localhost:3008/api/v1/health');
|
||||
console.log('✅ API proxy is working');
|
||||
console.log(' Health status:', response.data.status);
|
||||
} catch (error) {
|
||||
console.log('❌ API proxy not working:', error.message);
|
||||
}
|
||||
|
||||
// Test 3: Login endpoint
|
||||
try {
|
||||
const response = await axios.post('http://localhost:3008/api/v1/auth/login', {
|
||||
username: 'admin',
|
||||
password: 'admin123456'
|
||||
});
|
||||
console.log('✅ Login endpoint working');
|
||||
console.log(' Token:', response.data.data.accessToken.substring(0, 50) + '...');
|
||||
|
||||
// Test 4: Protected endpoint
|
||||
const token = response.data.data.accessToken;
|
||||
try {
|
||||
const dashResponse = await axios.get('http://localhost:3008/api/v1/analytics/dashboard', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
console.log('✅ Protected endpoints accessible');
|
||||
console.log(' Dashboard data received');
|
||||
} catch (error) {
|
||||
console.log('❌ Protected endpoint error:', error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ Login failed:', error.response?.data || error.message);
|
||||
}
|
||||
|
||||
console.log('\n=== Diagnosis Complete ===');
|
||||
}
|
||||
|
||||
diagnose();
|
||||
455
marketing-agent/docker-compose.prod.yml
Normal file
455
marketing-agent/docker-compose.prod.yml
Normal file
@@ -0,0 +1,455 @@
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
container_name: marketing_postgres
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# MongoDB for Events
|
||||
mongodb:
|
||||
image: mongo:5-focal
|
||||
container_name: marketing_mongodb
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: ${MONGO_USERNAME:-admin}
|
||||
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
|
||||
volumes:
|
||||
- mongodb_data:/data/db
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Redis for Cache and Queue
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: marketing_redis
|
||||
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# RabbitMQ Message Broker
|
||||
rabbitmq:
|
||||
image: rabbitmq:3-management-alpine
|
||||
container_name: marketing_rabbitmq
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
|
||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
|
||||
volumes:
|
||||
- rabbitmq_data:/var/lib/rabbitmq
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Elasticsearch (Optional for production)
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
|
||||
container_name: marketing_elasticsearch
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- xpack.security.enabled=true
|
||||
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
|
||||
- "ES_JAVA_OPTS=-Xms1g -Xmx1g"
|
||||
volumes:
|
||||
- elasticsearch_data:/usr/share/elasticsearch/data
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -s -u elastic:${ELASTIC_PASSWORD} http://localhost:9200/_cluster/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
# API Gateway Service
|
||||
api-gateway:
|
||||
build:
|
||||
context: ./services/api-gateway
|
||||
dockerfile: Dockerfile
|
||||
container_name: marketing_api_gateway
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
- MONGODB_URI=mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@mongodb:27017/marketing_agent?authSource=admin
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- ORCHESTRATOR_URL=http://orchestrator:3001
|
||||
- CLAUDE_AGENT_URL=http://claude-agent:3002
|
||||
- GRAMJS_ADAPTER_URL=http://gramjs-adapter:3003
|
||||
- SAFETY_GUARD_URL=http://safety-guard:3004
|
||||
- ANALYTICS_URL=http://analytics:3005
|
||||
- COMPLIANCE_GUARD_URL=http://compliance-guard:3006
|
||||
- AB_TESTING_URL=http://ab-testing:3007
|
||||
- TELEGRAM_SYSTEM_URL=${TELEGRAM_SYSTEM_URL}
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
mongodb:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: always
|
||||
deploy:
|
||||
replicas: 2
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1'
|
||||
memory: 1G
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Orchestrator Service
|
||||
orchestrator:
|
||||
build:
|
||||
context: ./services/orchestrator
|
||||
dockerfile: Dockerfile
|
||||
container_name: marketing_orchestrator
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- MONGODB_URI=mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@mongodb:27017/marketing_agent?authSource=admin
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
- RABBITMQ_URL=amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq:5672
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
depends_on:
|
||||
mongodb:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: always
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2'
|
||||
memory: 2G
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Claude Agent Service
|
||||
claude-agent:
|
||||
build:
|
||||
context: ./services/claude-agent
|
||||
dockerfile: Dockerfile
|
||||
container_name: marketing_claude_agent
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- MONGODB_URI=mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@mongodb:27017/marketing_agent?authSource=admin
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
- RABBITMQ_URL=amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq:5672
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
depends_on:
|
||||
mongodb:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: always
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2'
|
||||
memory: 4G
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3002/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# GramJS Adapter Service
|
||||
gramjs-adapter:
|
||||
build:
|
||||
context: ./services/gramjs-adapter
|
||||
dockerfile: Dockerfile
|
||||
container_name: marketing_gramjs_adapter
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- MONGODB_URI=mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@mongodb:27017/marketing_agent?authSource=admin
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
- RABBITMQ_URL=amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq:5672
|
||||
- TELEGRAM_SYSTEM_API_URL=${TELEGRAM_SYSTEM_URL}
|
||||
depends_on:
|
||||
mongodb:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: always
|
||||
deploy:
|
||||
replicas: 2
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2'
|
||||
memory: 2G
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3003/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Safety Guard Service
|
||||
safety-guard:
|
||||
build:
|
||||
context: ./services/safety-guard
|
||||
dockerfile: Dockerfile
|
||||
container_name: marketing_safety_guard
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- MONGODB_URI=mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@mongodb:27017/marketing_agent?authSource=admin
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
- RABBITMQ_URL=amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq:5672
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT}
|
||||
depends_on:
|
||||
mongodb:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: always
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1'
|
||||
memory: 1G
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3004/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Analytics Service
|
||||
analytics:
|
||||
build:
|
||||
context: ./services/analytics
|
||||
dockerfile: Dockerfile
|
||||
container_name: marketing_analytics
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- MONGODB_URI=mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@mongodb:27017/marketing_agent?authSource=admin
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
- CLICKHOUSE_HOST=${CLICKHOUSE_HOST:-clickhouse}
|
||||
- ELASTICSEARCH_HOST=elasticsearch:9200
|
||||
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
|
||||
- RABBITMQ_URL=amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq:5672
|
||||
depends_on:
|
||||
mongodb:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
elasticsearch:
|
||||
condition: service_healthy
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: always
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2'
|
||||
memory: 2G
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3005/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Compliance Guard Service
|
||||
compliance-guard:
|
||||
build:
|
||||
context: ./services/compliance-guard
|
||||
dockerfile: Dockerfile
|
||||
container_name: marketing_compliance_guard
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- MONGODB_URI=mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@mongodb:27017/marketing_agent?authSource=admin
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
- RABBITMQ_URL=amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq:5672
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
|
||||
depends_on:
|
||||
mongodb:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: always
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1'
|
||||
memory: 1G
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3006/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# A/B Testing Service
|
||||
ab-testing:
|
||||
build:
|
||||
context: ./services/ab-testing
|
||||
dockerfile: Dockerfile
|
||||
container_name: marketing_ab_testing
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- MONGODB_URI=mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@mongodb:27017/marketing_agent?authSource=admin
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||
- RABBITMQ_URL=amqp://${RABBITMQ_DEFAULT_USER}:${RABBITMQ_DEFAULT_PASS}@rabbitmq:5672
|
||||
depends_on:
|
||||
mongodb:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: always
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1'
|
||||
memory: 1G
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3007/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Nginx Reverse Proxy
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: marketing_nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./infrastructure/nginx/nginx.prod.conf:/etc/nginx/nginx.conf
|
||||
- ./infrastructure/nginx/conf.d:/etc/nginx/conf.d
|
||||
- ./infrastructure/ssl:/etc/ssl
|
||||
depends_on:
|
||||
api-gateway:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Prometheus
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: marketing_prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--storage.tsdb.retention.time=30d'
|
||||
volumes:
|
||||
- ./infrastructure/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus_data:/prometheus
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: always
|
||||
|
||||
# Grafana
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: marketing_grafana
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
- GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-simple-json-datasource
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
- ./infrastructure/grafana/provisioning:/etc/grafana/provisioning
|
||||
depends_on:
|
||||
- prometheus
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
mongodb_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
rabbitmq_data:
|
||||
driver: local
|
||||
elasticsearch_data:
|
||||
driver: local
|
||||
prometheus_data:
|
||||
driver: local
|
||||
grafana_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
marketing_network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
303
marketing-agent/docker-compose.yml
Normal file
303
marketing-agent/docker-compose.yml
Normal file
@@ -0,0 +1,303 @@
|
||||
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
image: postgres:14-alpine
|
||||
container_name: marketing_postgres
|
||||
environment:
|
||||
POSTGRES_USER: marketing_user
|
||||
POSTGRES_PASSWORD: marketing_pass
|
||||
POSTGRES_DB: marketing_agent
|
||||
ports:
|
||||
- "5433:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- marketing_network
|
||||
|
||||
# MongoDB for Events
|
||||
mongodb:
|
||||
image: mongo:5-focal
|
||||
container_name: marketing_mongodb
|
||||
ports:
|
||||
- "27018:27017"
|
||||
volumes:
|
||||
- mongodb_data:/data/db
|
||||
networks:
|
||||
- marketing_network
|
||||
|
||||
# Redis for Cache and Queue
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: marketing_redis
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- marketing_network
|
||||
|
||||
# RabbitMQ Message Broker
|
||||
rabbitmq:
|
||||
image: rabbitmq:3-management-alpine
|
||||
container_name: marketing_rabbitmq
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: admin
|
||||
RABBITMQ_DEFAULT_PASS: admin
|
||||
ports:
|
||||
- "5673:5672"
|
||||
- "15673:15672"
|
||||
volumes:
|
||||
- rabbitmq_data:/var/lib/rabbitmq
|
||||
networks:
|
||||
- marketing_network
|
||||
|
||||
# Elasticsearch
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
|
||||
container_name: marketing_elasticsearch
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- xpack.security.enabled=false
|
||||
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||||
ports:
|
||||
- "9201:9200"
|
||||
volumes:
|
||||
- elasticsearch_data:/usr/share/elasticsearch/data
|
||||
networks:
|
||||
- marketing_network
|
||||
|
||||
# Prometheus
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: marketing_prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./infrastructure/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus_data:/prometheus
|
||||
networks:
|
||||
- marketing_network
|
||||
|
||||
# Grafana
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: marketing_grafana
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
ports:
|
||||
- "3032:3000"
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
- ./infrastructure/grafana/provisioning:/etc/grafana/provisioning
|
||||
depends_on:
|
||||
- prometheus
|
||||
networks:
|
||||
- marketing_network
|
||||
|
||||
# API Gateway Service
|
||||
api-gateway:
|
||||
build: ./services/api-gateway
|
||||
container_name: marketing_api_gateway
|
||||
ports:
|
||||
- "3030:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- REDIS_HOST=redis
|
||||
- MONGODB_URI=mongodb://mongodb:27017/marketing_agent
|
||||
- JWT_SECRET=your-secret-key-change-in-production
|
||||
- ORCHESTRATOR_URL=http://orchestrator:3001
|
||||
- CLAUDE_AGENT_URL=http://claude-agent:3002
|
||||
- GRAMJS_ADAPTER_URL=http://gramjs-adapter:3003
|
||||
- SAFETY_GUARD_URL=http://safety-guard:3004
|
||||
- ANALYTICS_URL=http://analytics:3005
|
||||
- COMPLIANCE_GUARD_URL=http://compliance-guard:3006
|
||||
- AB_TESTING_URL=http://ab-testing:3007
|
||||
- TELEGRAM_SYSTEM_URL=http://host.docker.internal:8080
|
||||
depends_on:
|
||||
- redis
|
||||
- orchestrator
|
||||
- claude-agent
|
||||
- gramjs-adapter
|
||||
- safety-guard
|
||||
- analytics
|
||||
- compliance-guard
|
||||
- ab-testing
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: unless-stopped
|
||||
|
||||
# Orchestrator Service
|
||||
orchestrator:
|
||||
build: ./services/orchestrator
|
||||
container_name: marketing_orchestrator
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- MONGODB_URI=mongodb://mongodb:27017/marketing_agent
|
||||
- REDIS_HOST=redis
|
||||
- RABBITMQ_URL=amqp://admin:admin@rabbitmq:5672
|
||||
- JWT_SECRET=your-secret-key-change-in-production
|
||||
depends_on:
|
||||
- mongodb
|
||||
- redis
|
||||
- rabbitmq
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: unless-stopped
|
||||
|
||||
# Claude Agent Service
|
||||
claude-agent:
|
||||
build: ./services/claude-agent
|
||||
container_name: marketing_claude_agent
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- MONGODB_URI=mongodb://mongodb:27017/marketing_agent
|
||||
- REDIS_HOST=redis
|
||||
- RABBITMQ_URL=amqp://admin:admin@rabbitmq:5672
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
depends_on:
|
||||
- mongodb
|
||||
- redis
|
||||
- rabbitmq
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: unless-stopped
|
||||
|
||||
# GramJS Adapter Service
|
||||
gramjs-adapter:
|
||||
build: ./services/gramjs-adapter
|
||||
container_name: marketing_gramjs_adapter
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- MONGODB_URI=mongodb://mongodb:27017/marketing_agent
|
||||
- REDIS_HOST=redis
|
||||
- RABBITMQ_URL=amqp://admin:admin@rabbitmq:5672
|
||||
- TELEGRAM_SYSTEM_API_URL=http://host.docker.internal:8080
|
||||
volumes:
|
||||
- gramjs_sessions:/app/sessions
|
||||
depends_on:
|
||||
- mongodb
|
||||
- redis
|
||||
- rabbitmq
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: unless-stopped
|
||||
|
||||
# Safety Guard Service
|
||||
safety-guard:
|
||||
build: ./services/safety-guard
|
||||
container_name: marketing_safety_guard
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- MONGODB_URI=mongodb://mongodb:27017/marketing_agent
|
||||
- REDIS_HOST=redis
|
||||
- RABBITMQ_URL=amqp://admin:admin@rabbitmq:5672
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
||||
- GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT}
|
||||
depends_on:
|
||||
- mongodb
|
||||
- redis
|
||||
- rabbitmq
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: unless-stopped
|
||||
|
||||
# Analytics Service
|
||||
analytics:
|
||||
build: ./services/analytics
|
||||
container_name: marketing_analytics
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- MONGODB_URI=mongodb://mongodb:27017/marketing_agent
|
||||
- REDIS_HOST=redis
|
||||
- CLICKHOUSE_HOST=clickhouse
|
||||
- ELASTICSEARCH_HOST=elasticsearch:9200
|
||||
- RABBITMQ_URL=amqp://admin:admin@rabbitmq:5672
|
||||
depends_on:
|
||||
- mongodb
|
||||
- redis
|
||||
- elasticsearch
|
||||
- rabbitmq
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: unless-stopped
|
||||
|
||||
# Compliance Guard Service
|
||||
compliance-guard:
|
||||
build: ./services/compliance-guard
|
||||
container_name: marketing_compliance_guard
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- MONGODB_URI=mongodb://mongodb:27017/marketing_agent
|
||||
- REDIS_HOST=redis
|
||||
- RABBITMQ_URL=amqp://admin:admin@rabbitmq:5672
|
||||
- JWT_SECRET=your-secret-key-change-in-production
|
||||
depends_on:
|
||||
- mongodb
|
||||
- redis
|
||||
- rabbitmq
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: unless-stopped
|
||||
|
||||
# A/B Testing Service
|
||||
ab-testing:
|
||||
build: ./services/ab-testing
|
||||
container_name: marketing_ab_testing
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- MONGODB_URI=mongodb://mongodb:27017/marketing_agent
|
||||
- REDIS_HOST=redis
|
||||
- RABBITMQ_URL=amqp://admin:admin@rabbitmq:5672
|
||||
depends_on:
|
||||
- mongodb
|
||||
- redis
|
||||
- rabbitmq
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: unless-stopped
|
||||
|
||||
# Frontend Service
|
||||
frontend:
|
||||
build: ./frontend
|
||||
container_name: marketing_frontend
|
||||
ports:
|
||||
- "3008:80"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
depends_on:
|
||||
- api-gateway
|
||||
networks:
|
||||
- marketing_network
|
||||
restart: unless-stopped
|
||||
|
||||
# Nginx API Gateway
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: marketing_nginx
|
||||
ports:
|
||||
- "8000:80"
|
||||
volumes:
|
||||
- ./infrastructure/nginx/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./infrastructure/nginx/conf.d:/etc/nginx/conf.d
|
||||
depends_on:
|
||||
- api-gateway
|
||||
networks:
|
||||
- marketing_network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
mongodb_data:
|
||||
redis_data:
|
||||
rabbitmq_data:
|
||||
elasticsearch_data:
|
||||
prometheus_data:
|
||||
grafana_data:
|
||||
gramjs_sessions:
|
||||
|
||||
networks:
|
||||
marketing_network:
|
||||
driver: bridge
|
||||
243
marketing-agent/docs/QUICKSTART.md
Normal file
243
marketing-agent/docs/QUICKSTART.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# Quick Start Guide
|
||||
|
||||
Get started with the Telegram Marketing Agent System in 5 minutes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker and Docker Compose installed
|
||||
- Node.js 18+ (for local development)
|
||||
- MongoDB and Redis (or use Docker)
|
||||
- Telegram account with API credentials
|
||||
|
||||
## Installation
|
||||
|
||||
### Option 1: Docker Compose (Recommended)
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/telegram-marketing-agent.git
|
||||
cd telegram-marketing-agent
|
||||
```
|
||||
|
||||
2. Create environment file:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
3. Update `.env` with your configurations:
|
||||
```env
|
||||
# Telegram API
|
||||
TELEGRAM_API_ID=your_api_id
|
||||
TELEGRAM_API_HASH=your_api_hash
|
||||
|
||||
# Claude API (optional)
|
||||
CLAUDE_API_KEY=your_claude_key
|
||||
|
||||
# Database
|
||||
MONGODB_URI=mongodb://mongodb:27017/marketing_agent
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
# Security
|
||||
JWT_SECRET=your-secret-key-change-this
|
||||
```
|
||||
|
||||
4. Start all services:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
5. Access the application:
|
||||
- Frontend: http://localhost:8080
|
||||
- API Gateway: http://localhost:3000
|
||||
- API Docs: http://localhost:3000/api-docs
|
||||
|
||||
### Option 2: Local Development
|
||||
|
||||
1. Install dependencies for each service:
|
||||
```bash
|
||||
# API Gateway
|
||||
cd services/api-gateway
|
||||
npm install
|
||||
|
||||
# Orchestrator
|
||||
cd ../orchestrator
|
||||
npm install
|
||||
|
||||
# Continue for all services...
|
||||
```
|
||||
|
||||
2. Start MongoDB and Redis:
|
||||
```bash
|
||||
# Using Docker
|
||||
docker run -d -p 27017:27017 --name mongodb mongo
|
||||
docker run -d -p 6379:6379 --name redis redis
|
||||
```
|
||||
|
||||
3. Start services:
|
||||
```bash
|
||||
# In separate terminals
|
||||
cd services/api-gateway && npm start
|
||||
cd services/orchestrator && npm start
|
||||
cd services/gramjs-adapter && npm start
|
||||
# Continue for all services...
|
||||
```
|
||||
|
||||
4. Start frontend:
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## First Steps
|
||||
|
||||
### 1. Login
|
||||
|
||||
Default credentials:
|
||||
- Username: `admin`
|
||||
- Password: `password123`
|
||||
|
||||
### 2. Connect Telegram Account
|
||||
|
||||
1. Go to **Settings** → **Accounts**
|
||||
2. Click **Add Account**
|
||||
3. Enter your phone number
|
||||
4. Follow the verification process
|
||||
|
||||
### 3. Import Users
|
||||
|
||||
1. Go to **Users** → **Import**
|
||||
2. Download the sample CSV template
|
||||
3. Fill in user data
|
||||
4. Upload and import
|
||||
|
||||
### 4. Create Your First Campaign
|
||||
|
||||
1. Go to **Campaigns** → **Create New**
|
||||
2. Fill in campaign details:
|
||||
- Name: "Welcome Campaign"
|
||||
- Type: "Message"
|
||||
- Target: Select imported users
|
||||
3. Create message:
|
||||
- Use template or write custom
|
||||
- Add personalization tags
|
||||
4. Review and save as draft
|
||||
|
||||
### 5. Test Campaign
|
||||
|
||||
1. Select a small test group
|
||||
2. Click **Test Campaign**
|
||||
3. Review test results
|
||||
4. Make adjustments if needed
|
||||
|
||||
### 6. Execute Campaign
|
||||
|
||||
1. Click **Execute Campaign**
|
||||
2. Monitor real-time progress
|
||||
3. View analytics dashboard
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Creating User Segments
|
||||
|
||||
```javascript
|
||||
// API Example
|
||||
POST /api/v1/segments
|
||||
{
|
||||
"name": "Active Users",
|
||||
"criteria": [{
|
||||
"field": "engagement.lastActivity",
|
||||
"operator": "greater_than",
|
||||
"value": "7d"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### Scheduling Recurring Campaigns
|
||||
|
||||
1. Create campaign
|
||||
2. Go to **Schedules** → **Create Schedule**
|
||||
3. Select campaign and set recurrence:
|
||||
- Daily at 10 AM
|
||||
- Weekly on Mondays
|
||||
- Monthly on 1st
|
||||
|
||||
### Setting Up Webhooks
|
||||
|
||||
```javascript
|
||||
// API Example
|
||||
POST /api/v1/webhooks
|
||||
{
|
||||
"name": "Campaign Events",
|
||||
"url": "https://your-server.com/webhooks",
|
||||
"events": ["campaign.completed", "campaign.failed"]
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Cannot Connect to Telegram
|
||||
|
||||
1. Check API credentials in `.env`
|
||||
2. Ensure phone number format: `+1234567890`
|
||||
3. Check firewall/proxy settings
|
||||
4. View logs: `docker-compose logs gramjs-adapter`
|
||||
|
||||
### Campaign Not Sending
|
||||
|
||||
1. Check account connection status
|
||||
2. Verify rate limits aren't exceeded
|
||||
3. Check user permissions
|
||||
4. Review compliance settings
|
||||
|
||||
### Performance Issues
|
||||
|
||||
1. Check Redis connection
|
||||
2. Monitor resource usage
|
||||
3. Adjust rate limiting settings
|
||||
4. Scale services if needed
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Test First**: Always test campaigns on small groups
|
||||
2. **Rate Limiting**: Respect Telegram's rate limits
|
||||
3. **Personalization**: Use tags for better engagement
|
||||
4. **Timing**: Schedule during user's active hours
|
||||
5. **Compliance**: Follow local regulations
|
||||
6. **Monitoring**: Set up alerts for failures
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Read Full Documentation](./README.md)
|
||||
- [API Documentation](./api/README.md)
|
||||
- [Advanced Features](./ADVANCED.md)
|
||||
- [Deployment Guide](./DEPLOYMENT.md)
|
||||
|
||||
## Support
|
||||
|
||||
- GitHub Issues: [Report bugs](https://github.com/yourusername/telegram-marketing-agent/issues)
|
||||
- Documentation: [Full docs](./docs)
|
||||
- Community: [Join Discord](https://discord.gg/yourinvite)
|
||||
|
||||
## Quick API Reference
|
||||
|
||||
### Authentication
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/v1/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username": "admin", "password": "password123"}'
|
||||
```
|
||||
|
||||
### List Campaigns
|
||||
```bash
|
||||
curl -X GET http://localhost:3000/api/v1/campaigns \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
### Execute Campaign
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/v1/campaigns/<id>/execute \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
For more examples, visit the [API Examples](./api/examples.md) page.
|
||||
449
marketing-agent/docs/TESTING.md
Normal file
449
marketing-agent/docs/TESTING.md
Normal file
@@ -0,0 +1,449 @@
|
||||
# Testing Documentation
|
||||
|
||||
Comprehensive testing guide for the Telegram Marketing Agent System.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Testing Strategy](#testing-strategy)
|
||||
- [Test Types](#test-types)
|
||||
- [Running Tests](#running-tests)
|
||||
- [Writing Tests](#writing-tests)
|
||||
- [CI/CD Integration](#cicd-integration)
|
||||
- [Coverage Goals](#coverage-goals)
|
||||
- [Best Practices](#best-practices)
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Our testing strategy follows the Testing Pyramid approach:
|
||||
|
||||
```
|
||||
/\
|
||||
/E2E\ (5-10%)
|
||||
/------\
|
||||
/Integration\ (20-30%)
|
||||
/------------\
|
||||
/ Unit Tests \ (60-70%)
|
||||
/-----------------\
|
||||
```
|
||||
|
||||
### Test Categories
|
||||
|
||||
1. **Unit Tests**: Test individual functions, methods, and components in isolation
|
||||
2. **Integration Tests**: Test interactions between components and services
|
||||
3. **End-to-End Tests**: Test complete user workflows and scenarios
|
||||
|
||||
## Test Types
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Located in `tests/unit/`, these tests cover:
|
||||
- Service methods
|
||||
- Utility functions
|
||||
- Middleware logic
|
||||
- Model validations
|
||||
- Helper functions
|
||||
|
||||
Example structure:
|
||||
```
|
||||
tests/unit/
|
||||
├── services/
|
||||
│ ├── api-gateway/
|
||||
│ │ ├── middleware/
|
||||
│ │ │ ├── auth.test.js
|
||||
│ │ │ └── rateLimiter.test.js
|
||||
│ │ └── utils/
|
||||
│ └── orchestrator/
|
||||
│ └── campaignService.test.js
|
||||
└── utils/
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
Located in `tests/integration/`, these tests cover:
|
||||
- API endpoint functionality
|
||||
- Database operations
|
||||
- Service interactions
|
||||
- External API mocking
|
||||
|
||||
Example structure:
|
||||
```
|
||||
tests/integration/
|
||||
├── api/
|
||||
│ ├── auth.test.js
|
||||
│ ├── campaigns.test.js
|
||||
│ └── users.test.js
|
||||
└── services/
|
||||
```
|
||||
|
||||
### End-to-End Tests
|
||||
|
||||
Located in `tests/e2e/`, these tests cover:
|
||||
- Complete user workflows
|
||||
- Multi-service interactions
|
||||
- Real-world scenarios
|
||||
|
||||
Example structure:
|
||||
```
|
||||
tests/e2e/
|
||||
├── campaigns/
|
||||
│ └── campaignWorkflow.test.js
|
||||
└── users/
|
||||
└── userOnboarding.test.js
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# For each service
|
||||
cd services/<service-name>
|
||||
npm install
|
||||
```
|
||||
|
||||
### All Tests
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
### Unit Tests Only
|
||||
```bash
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Integration Tests Only
|
||||
```bash
|
||||
npm run test:integration
|
||||
```
|
||||
|
||||
### E2E Tests Only
|
||||
```bash
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### Watch Mode (for development)
|
||||
```bash
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
### Coverage Report
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
### Specific Test File
|
||||
```bash
|
||||
npx jest tests/unit/services/orchestrator/campaignService.test.js
|
||||
```
|
||||
|
||||
### Test Pattern
|
||||
```bash
|
||||
npx jest --testNamePattern="should create campaign"
|
||||
```
|
||||
|
||||
## Writing Tests
|
||||
|
||||
### Unit Test Example
|
||||
|
||||
```javascript
|
||||
import { jest } from '@jest/globals';
|
||||
import CampaignService from '../../../../services/orchestrator/src/services/campaignService.js';
|
||||
import { createCampaign } from '../../../helpers/factories.js';
|
||||
|
||||
describe('CampaignService', () => {
|
||||
let campaignService;
|
||||
let mockDependency;
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup mocks
|
||||
mockDependency = {
|
||||
method: jest.fn()
|
||||
};
|
||||
|
||||
campaignService = new CampaignService(mockDependency);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createCampaign', () => {
|
||||
it('should create a new campaign', async () => {
|
||||
// Arrange
|
||||
const campaignData = createCampaign();
|
||||
mockDependency.method.mockResolvedValue({ success: true });
|
||||
|
||||
// Act
|
||||
const result = await campaignService.createCampaign(campaignData);
|
||||
|
||||
// Assert
|
||||
expect(result).toHaveProperty('id');
|
||||
expect(mockDependency.method).toHaveBeenCalledWith(expect.any(Object));
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
// Arrange
|
||||
mockDependency.method.mockRejectedValue(new Error('Database error'));
|
||||
|
||||
// Act & Assert
|
||||
await expect(campaignService.createCampaign({}))
|
||||
.rejects.toThrow('Database error');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Test Example
|
||||
|
||||
```javascript
|
||||
import request from 'supertest';
|
||||
import app from '../../../services/api-gateway/src/app.js';
|
||||
import { connectDatabase, closeDatabase, clearDatabase } from '../../helpers/database.js';
|
||||
|
||||
describe('Campaigns API', () => {
|
||||
let authToken;
|
||||
|
||||
beforeAll(async () => {
|
||||
await connectDatabase();
|
||||
authToken = await getAuthToken();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await clearDatabase();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeDatabase();
|
||||
});
|
||||
|
||||
describe('POST /api/v1/campaigns', () => {
|
||||
it('should create campaign', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/v1/campaigns')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.send({
|
||||
name: 'Test Campaign',
|
||||
type: 'message'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.success).toBe(true);
|
||||
expect(response.body.data.campaign).toHaveProperty('id');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### E2E Test Example
|
||||
|
||||
```javascript
|
||||
describe('Campaign Workflow', () => {
|
||||
it('should complete full campaign lifecycle', async () => {
|
||||
// 1. Create template
|
||||
const template = await createTemplate();
|
||||
|
||||
// 2. Import users
|
||||
const users = await importUsers();
|
||||
|
||||
// 3. Create segment
|
||||
const segment = await createSegment();
|
||||
|
||||
// 4. Create campaign
|
||||
const campaign = await createCampaign({
|
||||
templateId: template.id,
|
||||
segmentId: segment.id
|
||||
});
|
||||
|
||||
// 5. Execute campaign
|
||||
const execution = await executeCampaign(campaign.id);
|
||||
|
||||
// 6. Verify results
|
||||
expect(execution.status).toBe('completed');
|
||||
expect(execution.messagesSent).toBe(users.length);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Test Helpers and Utilities
|
||||
|
||||
### Database Helpers
|
||||
```javascript
|
||||
import { connectDatabase, closeDatabase, clearDatabase } from './helpers/database.js';
|
||||
```
|
||||
|
||||
### Factory Functions
|
||||
```javascript
|
||||
import {
|
||||
createUser,
|
||||
createCampaign,
|
||||
createTemplate
|
||||
} from './helpers/factories.js';
|
||||
```
|
||||
|
||||
### Authentication Helpers
|
||||
```javascript
|
||||
import { generateAuthToken, createAuthenticatedRequest } from './helpers/auth.js';
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
Tests run automatically on:
|
||||
- Pull requests
|
||||
- Commits to main/develop branches
|
||||
- Before deployments
|
||||
|
||||
### GitHub Actions Workflow
|
||||
|
||||
See `.github/workflows/test.yml` for the complete CI configuration.
|
||||
|
||||
Key features:
|
||||
- Matrix testing (Node.js 18.x, 20.x)
|
||||
- Multiple database versions
|
||||
- Parallel test execution
|
||||
- Coverage reporting
|
||||
- Test result artifacts
|
||||
|
||||
### Pre-commit Hooks
|
||||
|
||||
```bash
|
||||
# Install husky
|
||||
npm prepare
|
||||
|
||||
# Pre-commit hook runs:
|
||||
- Linting
|
||||
- Unit tests for changed files
|
||||
- Commit message validation
|
||||
```
|
||||
|
||||
## Coverage Goals
|
||||
|
||||
### Overall Coverage Targets
|
||||
- **Statements**: 80%
|
||||
- **Branches**: 70%
|
||||
- **Functions**: 70%
|
||||
- **Lines**: 80%
|
||||
|
||||
### Service-Specific Targets
|
||||
- **API Gateway**: 85% (critical path)
|
||||
- **Orchestrator**: 80%
|
||||
- **Analytics**: 75%
|
||||
- **User Management**: 80%
|
||||
- **Scheduler**: 75%
|
||||
|
||||
### Viewing Coverage
|
||||
|
||||
```bash
|
||||
# Generate HTML coverage report
|
||||
npm run test:coverage
|
||||
|
||||
# Open in browser
|
||||
open coverage/lcov-report/index.html
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### General Guidelines
|
||||
|
||||
1. **Test Naming**: Use descriptive test names that explain what is being tested
|
||||
```javascript
|
||||
// Good
|
||||
it('should return 404 when campaign does not exist')
|
||||
|
||||
// Bad
|
||||
it('test campaign')
|
||||
```
|
||||
|
||||
2. **Arrange-Act-Assert**: Structure tests clearly
|
||||
```javascript
|
||||
it('should calculate discount correctly', () => {
|
||||
// Arrange
|
||||
const price = 100;
|
||||
const discountRate = 0.2;
|
||||
|
||||
// Act
|
||||
const result = calculateDiscount(price, discountRate);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(80);
|
||||
});
|
||||
```
|
||||
|
||||
3. **Isolation**: Each test should be independent
|
||||
- Use `beforeEach` and `afterEach` for setup/cleanup
|
||||
- Don't rely on test execution order
|
||||
- Clear mocks between tests
|
||||
|
||||
4. **Mocking**: Mock external dependencies
|
||||
```javascript
|
||||
jest.mock('axios');
|
||||
axios.get.mockResolvedValue({ data: mockData });
|
||||
```
|
||||
|
||||
5. **Async Testing**: Handle promises properly
|
||||
```javascript
|
||||
// Good
|
||||
it('should handle async operation', async () => {
|
||||
await expect(asyncFunction()).resolves.toBe(expected);
|
||||
});
|
||||
|
||||
// Also good
|
||||
it('should handle async operation', () => {
|
||||
return expect(asyncFunction()).resolves.toBe(expected);
|
||||
});
|
||||
```
|
||||
|
||||
6. **Error Testing**: Test error cases thoroughly
|
||||
```javascript
|
||||
it('should throw error for invalid input', async () => {
|
||||
await expect(functionUnderTest(null))
|
||||
.rejects.toThrow('Input cannot be null');
|
||||
});
|
||||
```
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
1. **Use Test Databases**: MongoDB Memory Server for unit tests
|
||||
2. **Parallel Execution**: Run independent tests in parallel
|
||||
3. **Selective Testing**: Use `--watch` mode during development
|
||||
4. **Mock Heavy Operations**: Mock file I/O, network calls
|
||||
|
||||
### Security Testing
|
||||
|
||||
1. **Authentication**: Test all auth scenarios
|
||||
2. **Authorization**: Verify role-based access
|
||||
3. **Input Validation**: Test with malicious inputs
|
||||
4. **Rate Limiting**: Verify limits are enforced
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Timeout Errors**
|
||||
```javascript
|
||||
// Increase timeout for specific test
|
||||
it('should handle long operation', async () => {
|
||||
// test code
|
||||
}, 10000); // 10 second timeout
|
||||
```
|
||||
|
||||
2. **Database Connection Issues**
|
||||
- Ensure MongoDB/Redis are running
|
||||
- Check connection strings in test environment
|
||||
- Clear test database between runs
|
||||
|
||||
3. **Flaky Tests**
|
||||
- Add proper waits for async operations
|
||||
- Mock time-dependent functions
|
||||
- Use stable test data
|
||||
|
||||
4. **Memory Leaks**
|
||||
- Close all connections in `afterAll`
|
||||
- Clear large data structures
|
||||
- Use `--detectLeaks` flag
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Jest Documentation](https://jestjs.io/docs/getting-started)
|
||||
- [Supertest Documentation](https://github.com/visionmedia/supertest)
|
||||
- [MongoDB Memory Server](https://github.com/nodkz/mongodb-memory-server)
|
||||
- [Testing Best Practices](https://github.com/goldbergyoni/javascript-testing-best-practices)
|
||||
155
marketing-agent/docs/api/CHANGELOG.md
Normal file
155
marketing-agent/docs/api/CHANGELOG.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# API Changelog
|
||||
|
||||
All notable changes to the Telegram Marketing Agent API will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.0] - 2024-01-14
|
||||
|
||||
### Added
|
||||
|
||||
#### Authentication & Security
|
||||
- JWT-based authentication with access and refresh tokens
|
||||
- API key authentication for programmatic access
|
||||
- Role-based access control (admin, user, viewer)
|
||||
- Rate limiting with Redis backend
|
||||
- Input validation and sanitization
|
||||
- SQL/NoSQL injection prevention
|
||||
|
||||
#### Campaign Management
|
||||
- Create, read, update, and delete campaigns
|
||||
- Multiple campaign types (message, invitation, data collection, engagement, custom)
|
||||
- Campaign execution with real-time progress tracking
|
||||
- Test mode for campaign validation
|
||||
- Campaign duplication functionality
|
||||
- Campaign statistics and analytics
|
||||
|
||||
#### Campaign Scheduling
|
||||
- One-time campaign scheduling
|
||||
- Recurring campaigns (daily, weekly, monthly, custom)
|
||||
- Trigger-based campaigns
|
||||
- Timezone support for schedules
|
||||
- Schedule preview functionality
|
||||
- Job management and retry mechanisms
|
||||
|
||||
#### User Management
|
||||
- CRUD operations for Telegram users
|
||||
- User grouping functionality
|
||||
- Tag-based user categorization
|
||||
- Dynamic user segmentation
|
||||
- Bulk user operations
|
||||
- CSV/Excel import/export
|
||||
- Custom user fields support
|
||||
|
||||
#### Analytics & Reporting
|
||||
- Real-time analytics dashboard
|
||||
- Campaign performance metrics
|
||||
- User engagement tracking
|
||||
- Conversion tracking
|
||||
- Revenue reporting
|
||||
- Time-series data
|
||||
- Export functionality
|
||||
|
||||
#### Message Templates
|
||||
- Multi-language template support
|
||||
- Variable interpolation
|
||||
- Template categories
|
||||
- Template versioning
|
||||
- A/B testing support
|
||||
|
||||
#### Workflow Automation
|
||||
- Multi-step workflow creation
|
||||
- Conditional logic
|
||||
- Action triggers
|
||||
- Workflow templates
|
||||
- Performance tracking
|
||||
|
||||
#### Webhook Integration
|
||||
- Event-based webhooks
|
||||
- Configurable event types
|
||||
- Retry mechanisms
|
||||
- Webhook testing
|
||||
- Event logs
|
||||
|
||||
#### Data Management
|
||||
- Automated backups
|
||||
- Data import/export
|
||||
- Compliance tools
|
||||
- Data retention policies
|
||||
|
||||
#### Claude AI Integration
|
||||
- AI-powered content suggestions
|
||||
- Campaign optimization recommendations
|
||||
- Audience insights
|
||||
- Performance predictions
|
||||
|
||||
### Security
|
||||
- HTTPS enforcement
|
||||
- CORS configuration
|
||||
- Helmet.js security headers
|
||||
- Request signing
|
||||
- API versioning
|
||||
|
||||
### Documentation
|
||||
- Comprehensive API documentation
|
||||
- Swagger/OpenAPI 3.0 specification
|
||||
- Interactive API explorer
|
||||
- Code examples in multiple languages
|
||||
- Postman collection
|
||||
- Quick start guide
|
||||
|
||||
## API Versioning
|
||||
|
||||
The API uses URL versioning. All endpoints are prefixed with `/api/v1/`.
|
||||
|
||||
## Breaking Changes Policy
|
||||
|
||||
- Breaking changes will only be introduced in major version releases
|
||||
- Deprecated features will be maintained for at least 6 months
|
||||
- Migration guides will be provided for all breaking changes
|
||||
|
||||
## Deprecation Notices
|
||||
|
||||
Currently, there are no deprecated endpoints.
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From Beta to v1.0.0
|
||||
|
||||
If you were using the beta version of the API, please note the following changes:
|
||||
|
||||
1. **Authentication**: The `/auth/token` endpoint has been renamed to `/auth/login`
|
||||
2. **User Management**: The `/telegram-users` endpoints have been moved to `/users`
|
||||
3. **Campaign Execution**: The `/campaigns/:id/send` endpoint is now `/campaigns/:id/execute`
|
||||
4. **Response Format**: All responses now follow a consistent format:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {},
|
||||
"meta": {}
|
||||
}
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For API support, please:
|
||||
- Check the [API Documentation](./README.md)
|
||||
- Review [Common Issues](./TROUBLESHOOTING.md)
|
||||
- Contact support at api-support@example.com
|
||||
|
||||
## Upcoming Features
|
||||
|
||||
### v1.1.0 (Planned)
|
||||
- GraphQL API endpoint
|
||||
- WebSocket support for real-time updates
|
||||
- Advanced analytics with custom metrics
|
||||
- Multi-account management
|
||||
- Enhanced AI capabilities
|
||||
|
||||
### v1.2.0 (Planned)
|
||||
- Video message support
|
||||
- Voice message campaigns
|
||||
- Interactive bot responses
|
||||
- Advanced segmentation with ML
|
||||
- Predictive analytics
|
||||
136
marketing-agent/docs/api/README.md
Normal file
136
marketing-agent/docs/api/README.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Telegram Marketing Agent System API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Telegram Marketing Agent System provides a comprehensive REST API for managing marketing campaigns, user segmentation, message scheduling, and analytics. This documentation covers all available endpoints, authentication requirements, and usage examples.
|
||||
|
||||
## Base URL
|
||||
|
||||
```
|
||||
http://localhost:3000/api/v1
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
All API endpoints (except login and public endpoints) require JWT authentication. Include the JWT token in the Authorization header:
|
||||
|
||||
```
|
||||
Authorization: Bearer <your-jwt-token>
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Login to get access token**
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/v1/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username": "admin", "password": "password123"}'
|
||||
```
|
||||
|
||||
2. **Use the token for subsequent requests**
|
||||
```bash
|
||||
curl -X GET http://localhost:3000/api/v1/campaigns \
|
||||
-H "Authorization: Bearer <your-token>"
|
||||
```
|
||||
|
||||
## API Documentation Sections
|
||||
|
||||
### Core Services
|
||||
|
||||
1. [Authentication API](./auth-api.md) - User authentication and session management
|
||||
2. [Campaigns API](./campaigns-api.md) - Campaign creation and management
|
||||
3. [Analytics API](./analytics-api.md) - Real-time analytics and reporting
|
||||
4. [Users API](./users-api.md) - User management and segmentation
|
||||
|
||||
### Marketing Features
|
||||
|
||||
5. [Templates API](./templates-api.md) - Message template management
|
||||
6. [Scheduled Campaigns API](./scheduled-campaigns-api.md) - Campaign scheduling
|
||||
7. [A/B Testing API](./ab-testing-api.md) - A/B test management
|
||||
8. [Workflows API](./workflows-api.md) - Marketing automation workflows
|
||||
|
||||
### Integration Services
|
||||
|
||||
9. [Webhooks API](./webhooks-api.md) - Webhook integration management
|
||||
10. [Translations API](./translations-api.md) - Multi-language support
|
||||
11. [AI API](./ai-api.md) - Claude AI integration for smart suggestions
|
||||
|
||||
### System Management
|
||||
|
||||
12. [Accounts API](./accounts-api.md) - Telegram account management
|
||||
13. [Compliance API](./compliance-api.md) - Compliance checking
|
||||
14. [Settings API](./settings-api.md) - System configuration
|
||||
|
||||
## Interactive API Explorer
|
||||
|
||||
Access the Swagger UI at: `http://localhost:3000/api-docs`
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
- Default rate limit: 100 requests per minute per IP
|
||||
- Authenticated users: 1000 requests per minute
|
||||
- AI endpoints: 20 requests per minute
|
||||
|
||||
## Error Handling
|
||||
|
||||
All API errors follow a consistent format:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Error message",
|
||||
"code": "ERROR_CODE",
|
||||
"details": {}
|
||||
}
|
||||
```
|
||||
|
||||
### Common Error Codes
|
||||
|
||||
- `400` - Bad Request
|
||||
- `401` - Unauthorized
|
||||
- `403` - Forbidden
|
||||
- `404` - Not Found
|
||||
- `429` - Too Many Requests
|
||||
- `500` - Internal Server Error
|
||||
|
||||
## Pagination
|
||||
|
||||
List endpoints support pagination:
|
||||
|
||||
```
|
||||
GET /api/v1/campaigns?page=1&limit=20&sort=-createdAt
|
||||
```
|
||||
|
||||
- `page` - Page number (default: 1)
|
||||
- `limit` - Items per page (default: 20, max: 100)
|
||||
- `sort` - Sort field, prefix with `-` for descending
|
||||
|
||||
## Filtering
|
||||
|
||||
Most list endpoints support filtering:
|
||||
|
||||
```
|
||||
GET /api/v1/campaigns?status=active&type=message
|
||||
```
|
||||
|
||||
## Webhooks
|
||||
|
||||
Configure webhooks to receive real-time notifications:
|
||||
|
||||
1. Register webhook endpoint
|
||||
2. Verify webhook signature
|
||||
3. Handle webhook events
|
||||
|
||||
See [Webhooks API](./webhooks-api.md) for details.
|
||||
|
||||
## SDKs and Libraries
|
||||
|
||||
- [Node.js SDK](https://github.com/yourusername/tg-marketing-sdk-node)
|
||||
- [Python SDK](https://github.com/yourusername/tg-marketing-sdk-python)
|
||||
- [PHP SDK](https://github.com/yourusername/tg-marketing-sdk-php)
|
||||
|
||||
## Support
|
||||
|
||||
- API Status: `http://localhost:3000/health`
|
||||
- Contact: api-support@yourcompany.com
|
||||
- GitHub: https://github.com/yourusername/telegram-marketing-agent
|
||||
380
marketing-agent/docs/api/TROUBLESHOOTING.md
Normal file
380
marketing-agent/docs/api/TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,380 @@
|
||||
# API Troubleshooting Guide
|
||||
|
||||
This guide helps you diagnose and resolve common issues with the Telegram Marketing Agent API.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Authentication Issues](#authentication-issues)
|
||||
- [Rate Limiting](#rate-limiting)
|
||||
- [Campaign Execution Problems](#campaign-execution-problems)
|
||||
- [Data Import/Export Issues](#data-importexport-issues)
|
||||
- [Webhook Problems](#webhook-problems)
|
||||
- [Performance Issues](#performance-issues)
|
||||
- [Error Codes Reference](#error-codes-reference)
|
||||
|
||||
## Authentication Issues
|
||||
|
||||
### Problem: 401 Unauthorized Error
|
||||
|
||||
**Symptoms:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Unauthorized",
|
||||
"code": "UNAUTHORIZED"
|
||||
}
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Check Token Format**
|
||||
```bash
|
||||
# Correct format
|
||||
curl -H "Authorization: Bearer YOUR_TOKEN" https://api.example.com/v1/users
|
||||
|
||||
# Common mistakes
|
||||
curl -H "Authorization: YOUR_TOKEN" # Missing "Bearer" prefix
|
||||
curl -H "Authorization: bearer YOUR_TOKEN" # Lowercase "bearer"
|
||||
```
|
||||
|
||||
2. **Verify Token Expiration**
|
||||
- Access tokens expire after 24 hours
|
||||
- Use the refresh token endpoint to get a new access token
|
||||
```bash
|
||||
curl -X POST https://api.example.com/v1/auth/refresh \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"refreshToken": "YOUR_REFRESH_TOKEN"}'
|
||||
```
|
||||
|
||||
3. **Check API Key (if using)**
|
||||
```bash
|
||||
curl -H "X-API-Key: YOUR_API_KEY" https://api.example.com/v1/users
|
||||
```
|
||||
|
||||
### Problem: 403 Forbidden Error
|
||||
|
||||
**Symptoms:**
|
||||
- User authenticated but lacks permissions
|
||||
- Specific endpoints return forbidden
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Check user role and permissions
|
||||
2. Verify endpoint access requirements
|
||||
3. Contact admin for permission updates
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Problem: 429 Too Many Requests
|
||||
|
||||
**Symptoms:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Too many requests",
|
||||
"code": "RATE_LIMIT_EXCEEDED",
|
||||
"details": {
|
||||
"retryAfter": 60
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Check Rate Limit Headers**
|
||||
```
|
||||
X-RateLimit-Limit: 100
|
||||
X-RateLimit-Remaining: 0
|
||||
X-RateLimit-Reset: 1642012800
|
||||
```
|
||||
|
||||
2. **Implement Exponential Backoff**
|
||||
```javascript
|
||||
async function retryWithBackoff(fn, maxRetries = 3) {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
if (error.status === 429 && i < maxRetries - 1) {
|
||||
const delay = Math.pow(2, i) * 1000;
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Batch Operations**
|
||||
- Use bulk endpoints when available
|
||||
- Group multiple operations into single requests
|
||||
|
||||
## Campaign Execution Problems
|
||||
|
||||
### Problem: Campaign Fails to Execute
|
||||
|
||||
**Symptoms:**
|
||||
- Campaign status remains "draft"
|
||||
- Execution endpoint returns errors
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Validate Campaign Configuration**
|
||||
```bash
|
||||
# Check campaign details
|
||||
curl -X GET https://api.example.com/v1/orchestrator/campaigns/CAMPAIGN_ID \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
2. **Common Issues:**
|
||||
- Empty target audience
|
||||
- Missing message content
|
||||
- Invalid scheduling parameters
|
||||
- Telegram account not connected
|
||||
|
||||
3. **Test Mode First**
|
||||
```bash
|
||||
curl -X POST https://api.example.com/v1/orchestrator/campaigns/CAMPAIGN_ID/execute \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"test": true,
|
||||
"testUsers": ["user_123", "user_456"]
|
||||
}'
|
||||
```
|
||||
|
||||
### Problem: Low Delivery Rate
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Check Rate Limits**
|
||||
- Telegram has strict rate limits
|
||||
- Reduce messages per second in campaign settings
|
||||
|
||||
2. **Verify User Status**
|
||||
- Check if users have blocked the bot
|
||||
- Ensure phone numbers are valid
|
||||
|
||||
3. **Monitor Telegram Account Health**
|
||||
- Check for account restrictions
|
||||
- Verify account connection status
|
||||
|
||||
## Data Import/Export Issues
|
||||
|
||||
### Problem: Import Fails with Validation Errors
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Validate CSV Format**
|
||||
```csv
|
||||
telegramId,username,firstName,lastName,phoneNumber,tags
|
||||
123456789,johndoe,John,Doe,+1234567890,"customer,active"
|
||||
```
|
||||
|
||||
2. **Common Issues:**
|
||||
- Missing required fields
|
||||
- Invalid phone number format
|
||||
- Special characters in CSV
|
||||
- File encoding (use UTF-8)
|
||||
|
||||
3. **Use Template**
|
||||
```bash
|
||||
# Download template
|
||||
curl -X GET https://api.example.com/v1/users/import/template \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-o user_template.csv
|
||||
```
|
||||
|
||||
### Problem: Export Timeout for Large Datasets
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Use Filters**
|
||||
```json
|
||||
{
|
||||
"format": "csv",
|
||||
"filters": {
|
||||
"status": "active",
|
||||
"createdAt": {
|
||||
"from": "2024-01-01",
|
||||
"to": "2024-01-31"
|
||||
}
|
||||
},
|
||||
"limit": 10000
|
||||
}
|
||||
```
|
||||
|
||||
2. **Paginated Export**
|
||||
- Export in batches
|
||||
- Use background job for large exports
|
||||
|
||||
## Webhook Problems
|
||||
|
||||
### Problem: Webhooks Not Triggering
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Verify Webhook Configuration**
|
||||
```bash
|
||||
curl -X GET https://api.example.com/v1/webhooks/WEBHOOK_ID \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
2. **Test Webhook**
|
||||
```bash
|
||||
curl -X POST https://api.example.com/v1/webhooks/WEBHOOK_ID/test \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"event": "campaign.completed",
|
||||
"payload": {"test": true}
|
||||
}'
|
||||
```
|
||||
|
||||
3. **Common Issues:**
|
||||
- URL not publicly accessible
|
||||
- SSL certificate problems
|
||||
- Timeout (webhook must respond within 10s)
|
||||
- Response status not 2xx
|
||||
|
||||
### Problem: Duplicate Webhook Events
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Implement idempotency using event IDs
|
||||
2. Check webhook logs for retry attempts
|
||||
3. Ensure webhook responds quickly
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### Problem: Slow API Response Times
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Use Caching Headers**
|
||||
```bash
|
||||
# Check if response is cached
|
||||
curl -I https://api.example.com/v1/analytics/dashboard \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
2. **Optimize Queries**
|
||||
- Use specific field selection
|
||||
- Implement pagination
|
||||
- Add appropriate filters
|
||||
|
||||
3. **Batch Operations**
|
||||
```json
|
||||
{
|
||||
"operations": [
|
||||
{"method": "GET", "path": "/users/user_123"},
|
||||
{"method": "GET", "path": "/users/user_456"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Problem: Timeout Errors
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. **Increase Client Timeout**
|
||||
```javascript
|
||||
const response = await fetch(url, {
|
||||
timeout: 30000 // 30 seconds
|
||||
});
|
||||
```
|
||||
|
||||
2. **Use Async Operations**
|
||||
- For long-running tasks, use job queues
|
||||
- Poll for results
|
||||
|
||||
## Error Codes Reference
|
||||
|
||||
### HTTP Status Codes
|
||||
|
||||
| Code | Meaning | Common Causes |
|
||||
|------|---------|---------------|
|
||||
| 400 | Bad Request | Invalid parameters, malformed JSON |
|
||||
| 401 | Unauthorized | Missing/invalid token, expired token |
|
||||
| 403 | Forbidden | Insufficient permissions |
|
||||
| 404 | Not Found | Resource doesn't exist |
|
||||
| 409 | Conflict | Duplicate resource, conflicting state |
|
||||
| 422 | Unprocessable Entity | Validation errors |
|
||||
| 429 | Too Many Requests | Rate limit exceeded |
|
||||
| 500 | Internal Server Error | Server-side issue |
|
||||
| 502 | Bad Gateway | Service unavailable |
|
||||
| 503 | Service Unavailable | Maintenance, overload |
|
||||
|
||||
### Application Error Codes
|
||||
|
||||
| Code | Description | Solution |
|
||||
|------|-------------|----------|
|
||||
| `AUTH_FAILED` | Authentication failed | Check credentials |
|
||||
| `TOKEN_EXPIRED` | Access token expired | Refresh token |
|
||||
| `INVALID_INPUT` | Input validation failed | Check request body |
|
||||
| `RESOURCE_NOT_FOUND` | Requested resource not found | Verify ID/path |
|
||||
| `DUPLICATE_RESOURCE` | Resource already exists | Use update instead |
|
||||
| `CAMPAIGN_NOT_READY` | Campaign missing required data | Complete campaign setup |
|
||||
| `TELEGRAM_ERROR` | Telegram API error | Check account status |
|
||||
| `QUOTA_EXCEEDED` | Account quota exceeded | Upgrade plan |
|
||||
|
||||
## Debug Mode
|
||||
|
||||
Enable debug mode for detailed error information:
|
||||
|
||||
```bash
|
||||
curl -X GET https://api.example.com/v1/users \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "X-Debug-Mode: true"
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you're still experiencing issues:
|
||||
|
||||
1. **Check Logs**
|
||||
- Request ID in response headers
|
||||
- Include in support tickets
|
||||
|
||||
2. **API Status**
|
||||
- Check https://status.example.com
|
||||
- Follow @api_status for updates
|
||||
|
||||
3. **Support Channels**
|
||||
- Email: api-support@example.com
|
||||
- Discord: https://discord.gg/example
|
||||
- GitHub Issues: https://github.com/example/api/issues
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always Handle Errors**
|
||||
```javascript
|
||||
try {
|
||||
const response = await api.createCampaign(data);
|
||||
} catch (error) {
|
||||
if (error.code === 'RATE_LIMIT_EXCEEDED') {
|
||||
// Handle rate limit
|
||||
} else if (error.code === 'VALIDATION_ERROR') {
|
||||
// Handle validation errors
|
||||
} else {
|
||||
// Handle other errors
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Log Everything**
|
||||
- Request/response bodies
|
||||
- Headers
|
||||
- Timestamps
|
||||
- Error messages
|
||||
|
||||
3. **Monitor Your Integration**
|
||||
- Set up alerts for failures
|
||||
- Track success rates
|
||||
- Monitor response times
|
||||
|
||||
4. **Use SDK When Available**
|
||||
- Automatic retry logic
|
||||
- Built-in error handling
|
||||
- Type safety
|
||||
389
marketing-agent/docs/api/auth-api.md
Normal file
389
marketing-agent/docs/api/auth-api.md
Normal file
@@ -0,0 +1,389 @@
|
||||
# Authentication API
|
||||
|
||||
The Authentication API manages user authentication, session management, and access control.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Login
|
||||
|
||||
Authenticate a user and receive access tokens.
|
||||
|
||||
```http
|
||||
POST /api/v1/auth/login
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"user": {
|
||||
"id": "user123",
|
||||
"username": "admin",
|
||||
"email": "admin@example.com",
|
||||
"role": "admin",
|
||||
"accountId": "acc123"
|
||||
},
|
||||
"tokens": {
|
||||
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"expiresIn": 86400
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/v1/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "admin",
|
||||
"password": "password123"
|
||||
}'
|
||||
```
|
||||
|
||||
### Register
|
||||
|
||||
Create a new user account.
|
||||
|
||||
```http
|
||||
POST /api/v1/auth/register
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "newuser",
|
||||
"email": "user@example.com",
|
||||
"password": "securepassword123",
|
||||
"fullName": "John Doe"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"user": {
|
||||
"id": "user456",
|
||||
"username": "newuser",
|
||||
"email": "user@example.com",
|
||||
"role": "user",
|
||||
"accountId": "acc456"
|
||||
},
|
||||
"message": "Registration successful. Please verify your email."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Refresh Token
|
||||
|
||||
Refresh access token using refresh token.
|
||||
|
||||
```http
|
||||
POST /api/v1/auth/refresh
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"expiresIn": 86400
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Logout
|
||||
|
||||
Invalidate current session.
|
||||
|
||||
```http
|
||||
POST /api/v1/auth/logout
|
||||
```
|
||||
|
||||
#### Headers
|
||||
|
||||
```
|
||||
Authorization: Bearer <access-token>
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"message": "Logged out successfully"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get Current User
|
||||
|
||||
Get authenticated user's profile.
|
||||
|
||||
```http
|
||||
GET /api/v1/auth/me
|
||||
```
|
||||
|
||||
#### Headers
|
||||
|
||||
```
|
||||
Authorization: Bearer <access-token>
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "user123",
|
||||
"username": "admin",
|
||||
"email": "admin@example.com",
|
||||
"fullName": "Admin User",
|
||||
"role": "admin",
|
||||
"accountId": "acc123",
|
||||
"permissions": [
|
||||
"campaigns.create",
|
||||
"campaigns.update",
|
||||
"campaigns.delete",
|
||||
"users.manage"
|
||||
],
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"lastLogin": "2024-01-20T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Update Profile
|
||||
|
||||
Update authenticated user's profile.
|
||||
|
||||
```http
|
||||
PUT /api/v1/auth/profile
|
||||
```
|
||||
|
||||
#### Headers
|
||||
|
||||
```
|
||||
Authorization: Bearer <access-token>
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"fullName": "John Smith",
|
||||
"email": "john.smith@example.com",
|
||||
"preferences": {
|
||||
"language": "en",
|
||||
"timezone": "America/New_York",
|
||||
"notifications": {
|
||||
"email": true,
|
||||
"push": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"message": "Profile updated successfully",
|
||||
"user": {
|
||||
"id": "user123",
|
||||
"username": "admin",
|
||||
"email": "john.smith@example.com",
|
||||
"fullName": "John Smith"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Change Password
|
||||
|
||||
Change authenticated user's password.
|
||||
|
||||
```http
|
||||
POST /api/v1/auth/change-password
|
||||
```
|
||||
|
||||
#### Headers
|
||||
|
||||
```
|
||||
Authorization: Bearer <access-token>
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"currentPassword": "oldpassword123",
|
||||
"newPassword": "newpassword456"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"message": "Password changed successfully"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reset Password Request
|
||||
|
||||
Request password reset link.
|
||||
|
||||
```http
|
||||
POST /api/v1/auth/forgot-password
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"message": "Password reset instructions sent to your email"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reset Password
|
||||
|
||||
Reset password using token.
|
||||
|
||||
```http
|
||||
POST /api/v1/auth/reset-password
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "reset-token-from-email",
|
||||
"newPassword": "newsecurepassword789"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"message": "Password reset successfully"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
### Invalid Credentials
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Invalid username or password",
|
||||
"code": "INVALID_CREDENTIALS"
|
||||
}
|
||||
```
|
||||
|
||||
### Token Expired
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Token has expired",
|
||||
"code": "TOKEN_EXPIRED"
|
||||
}
|
||||
```
|
||||
|
||||
### Account Locked
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Account is locked due to multiple failed login attempts",
|
||||
"code": "ACCOUNT_LOCKED",
|
||||
"details": {
|
||||
"lockedUntil": "2024-01-20T11:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Token Storage**: Store tokens securely in httpOnly cookies or secure storage
|
||||
2. **Token Rotation**: Refresh tokens regularly to minimize exposure
|
||||
3. **Password Requirements**:
|
||||
- Minimum 8 characters
|
||||
- At least one uppercase letter
|
||||
- At least one number
|
||||
- At least one special character
|
||||
4. **Rate Limiting**: Login attempts are rate-limited to prevent brute force attacks
|
||||
5. **Two-Factor Authentication**: Available for enhanced security (see 2FA endpoints)
|
||||
|
||||
## Two-Factor Authentication (2FA)
|
||||
|
||||
### Enable 2FA
|
||||
|
||||
```http
|
||||
POST /api/v1/auth/2fa/enable
|
||||
```
|
||||
|
||||
### Verify 2FA
|
||||
|
||||
```http
|
||||
POST /api/v1/auth/2fa/verify
|
||||
```
|
||||
|
||||
### Disable 2FA
|
||||
|
||||
```http
|
||||
POST /api/v1/auth/2fa/disable
|
||||
```
|
||||
|
||||
For detailed 2FA documentation, see the dedicated 2FA guide.
|
||||
484
marketing-agent/docs/api/campaigns-api.md
Normal file
484
marketing-agent/docs/api/campaigns-api.md
Normal file
@@ -0,0 +1,484 @@
|
||||
# Campaigns API
|
||||
|
||||
The Campaigns API allows you to create, manage, and execute marketing campaigns.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### List Campaigns
|
||||
|
||||
Get a paginated list of campaigns.
|
||||
|
||||
```http
|
||||
GET /api/v1/campaigns
|
||||
```
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Parameter | Type | Description | Default |
|
||||
|-----------|------|-------------|---------|
|
||||
| page | integer | Page number | 1 |
|
||||
| limit | integer | Items per page (max 100) | 20 |
|
||||
| status | string | Filter by status (draft, active, paused, completed, cancelled) | - |
|
||||
| type | string | Filter by type (message, invitation, data_collection, engagement, custom) | - |
|
||||
| sort | string | Sort field (prefix with - for descending) | -createdAt |
|
||||
| search | string | Search in name and description | - |
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"campaigns": [
|
||||
{
|
||||
"id": "camp123",
|
||||
"name": "Summer Sale Campaign",
|
||||
"description": "Promotional campaign for summer products",
|
||||
"type": "message",
|
||||
"status": "active",
|
||||
"goals": {
|
||||
"targetAudience": 10000,
|
||||
"conversionRate": 5,
|
||||
"revenue": 50000
|
||||
},
|
||||
"targetAudience": {
|
||||
"segments": ["seg123", "seg456"],
|
||||
"totalCount": 8500
|
||||
},
|
||||
"strategy": {
|
||||
"messaging": "Focus on discount offers",
|
||||
"timing": "Send during peak hours",
|
||||
"channels": ["telegram"]
|
||||
},
|
||||
"budget": 5000,
|
||||
"startDate": "2024-06-01T00:00:00Z",
|
||||
"endDate": "2024-08-31T23:59:59Z",
|
||||
"statistics": {
|
||||
"totalTasks": 100,
|
||||
"completedTasks": 45,
|
||||
"failedTasks": 2,
|
||||
"messagesSent": 4500,
|
||||
"conversionsAchieved": 225,
|
||||
"totalCost": 2250
|
||||
},
|
||||
"createdBy": "user123",
|
||||
"createdAt": "2024-05-15T10:00:00Z",
|
||||
"updatedAt": "2024-06-15T14:30:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total": 45,
|
||||
"pages": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:3000/api/v1/campaigns?status=active&limit=10" \
|
||||
-H "Authorization: Bearer <your-token>"
|
||||
```
|
||||
|
||||
### Get Campaign
|
||||
|
||||
Get a specific campaign by ID.
|
||||
|
||||
```http
|
||||
GET /api/v1/campaigns/:id
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "camp123",
|
||||
"name": "Summer Sale Campaign",
|
||||
"description": "Promotional campaign for summer products",
|
||||
"type": "message",
|
||||
"status": "active",
|
||||
"goals": {
|
||||
"targetAudience": 10000,
|
||||
"conversionRate": 5,
|
||||
"revenue": 50000
|
||||
},
|
||||
"targetAudience": {
|
||||
"segments": ["seg123", "seg456"],
|
||||
"filters": {
|
||||
"location": ["US", "CA"],
|
||||
"ageRange": [18, 45],
|
||||
"interests": ["shopping", "fashion"]
|
||||
},
|
||||
"totalCount": 8500
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg123",
|
||||
"templateId": "tmpl123",
|
||||
"content": "🌞 Summer Sale! Get 30% off on all items!",
|
||||
"variations": [
|
||||
{
|
||||
"id": "var1",
|
||||
"content": "☀️ Hot Summer Deals! Save 30% today!",
|
||||
"weight": 50
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"workflow": {
|
||||
"id": "wf123",
|
||||
"name": "Summer Sale Workflow",
|
||||
"triggers": ["manual", "scheduled"]
|
||||
},
|
||||
"abTests": [
|
||||
{
|
||||
"id": "ab123",
|
||||
"name": "Message Variation Test",
|
||||
"status": "running"
|
||||
}
|
||||
],
|
||||
"createdAt": "2024-05-15T10:00:00Z",
|
||||
"updatedAt": "2024-06-15T14:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Create Campaign
|
||||
|
||||
Create a new marketing campaign.
|
||||
|
||||
```http
|
||||
POST /api/v1/campaigns
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Black Friday Campaign",
|
||||
"description": "Massive discounts for Black Friday",
|
||||
"type": "message",
|
||||
"goals": {
|
||||
"targetAudience": 20000,
|
||||
"conversionRate": 10,
|
||||
"revenue": 100000
|
||||
},
|
||||
"targetAudience": {
|
||||
"segments": ["seg789"],
|
||||
"filters": {
|
||||
"location": ["US"],
|
||||
"purchaseHistory": true
|
||||
}
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"templateId": "tmpl456",
|
||||
"personalization": {
|
||||
"enabled": true,
|
||||
"fields": ["firstName", "lastPurchase"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"budget": 10000,
|
||||
"startDate": "2024-11-24T00:00:00Z",
|
||||
"endDate": "2024-11-30T23:59:59Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "camp456",
|
||||
"name": "Black Friday Campaign",
|
||||
"status": "draft",
|
||||
"message": "Campaign created successfully"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Update Campaign
|
||||
|
||||
Update an existing campaign.
|
||||
|
||||
```http
|
||||
PUT /api/v1/campaigns/:id
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Updated Campaign Name",
|
||||
"description": "Updated description",
|
||||
"goals": {
|
||||
"targetAudience": 25000,
|
||||
"conversionRate": 12
|
||||
},
|
||||
"status": "active"
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Campaign
|
||||
|
||||
Delete a campaign (only if status is draft or cancelled).
|
||||
|
||||
```http
|
||||
DELETE /api/v1/campaigns/:id
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"message": "Campaign deleted successfully"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Execute Campaign
|
||||
|
||||
Start executing a campaign.
|
||||
|
||||
```http
|
||||
POST /api/v1/campaigns/:id/execute
|
||||
```
|
||||
|
||||
#### Request Body (Optional)
|
||||
|
||||
```json
|
||||
{
|
||||
"testMode": false,
|
||||
"targetPercentage": 100,
|
||||
"priority": "high",
|
||||
"scheduledAt": "2024-11-24T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"executionId": "exec123",
|
||||
"status": "started",
|
||||
"estimatedCompletion": "2024-11-24T12:00:00Z",
|
||||
"message": "Campaign execution started"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pause Campaign
|
||||
|
||||
Pause an active campaign.
|
||||
|
||||
```http
|
||||
POST /api/v1/campaigns/:id/pause
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"status": "paused",
|
||||
"message": "Campaign paused successfully",
|
||||
"pausedAt": "2024-06-20T15:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Resume Campaign
|
||||
|
||||
Resume a paused campaign.
|
||||
|
||||
```http
|
||||
POST /api/v1/campaigns/:id/resume
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"status": "active",
|
||||
"message": "Campaign resumed successfully",
|
||||
"resumedAt": "2024-06-21T09:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Clone Campaign
|
||||
|
||||
Create a copy of an existing campaign.
|
||||
|
||||
```http
|
||||
POST /api/v1/campaigns/:id/clone
|
||||
```
|
||||
|
||||
#### Request Body (Optional)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Cloned Campaign Name",
|
||||
"resetStatistics": true
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "camp789",
|
||||
"name": "Summer Sale Campaign (Copy)",
|
||||
"status": "draft",
|
||||
"message": "Campaign cloned successfully"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get Campaign Statistics
|
||||
|
||||
Get detailed statistics for a campaign.
|
||||
|
||||
```http
|
||||
GET /api/v1/campaigns/:id/statistics
|
||||
```
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Parameter | Type | Description | Default |
|
||||
|-----------|------|-------------|---------|
|
||||
| period | string | Time period (1h, 24h, 7d, 30d, all) | all |
|
||||
| metrics | string | Comma-separated metrics to include | all |
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"overview": {
|
||||
"messagesSent": 8500,
|
||||
"delivered": 8200,
|
||||
"read": 6500,
|
||||
"clicked": 1200,
|
||||
"converted": 425,
|
||||
"revenue": 42500
|
||||
},
|
||||
"performance": {
|
||||
"deliveryRate": 96.5,
|
||||
"readRate": 76.5,
|
||||
"clickRate": 14.1,
|
||||
"conversionRate": 5.0
|
||||
},
|
||||
"timeline": [
|
||||
{
|
||||
"date": "2024-06-01",
|
||||
"sent": 1000,
|
||||
"delivered": 960,
|
||||
"conversions": 48
|
||||
}
|
||||
],
|
||||
"segments": [
|
||||
{
|
||||
"segmentId": "seg123",
|
||||
"name": "Premium Users",
|
||||
"performance": {
|
||||
"sent": 2000,
|
||||
"conversionRate": 8.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get Campaign Progress
|
||||
|
||||
Get real-time execution progress.
|
||||
|
||||
```http
|
||||
GET /api/v1/campaigns/:id/progress
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"status": "running",
|
||||
"progress": {
|
||||
"percentage": 65,
|
||||
"processed": 5525,
|
||||
"total": 8500,
|
||||
"successful": 5300,
|
||||
"failed": 225
|
||||
},
|
||||
"currentRate": 120,
|
||||
"estimatedCompletion": "2024-06-20T16:45:00Z",
|
||||
"errors": [
|
||||
{
|
||||
"code": "USER_BLOCKED",
|
||||
"count": 150,
|
||||
"message": "User has blocked the bot"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Campaign Types
|
||||
|
||||
### Message Campaign
|
||||
Send promotional or informational messages to users.
|
||||
|
||||
### Invitation Campaign
|
||||
Invite users to join groups or channels.
|
||||
|
||||
### Data Collection Campaign
|
||||
Collect user feedback or information through surveys.
|
||||
|
||||
### Engagement Campaign
|
||||
Increase user interaction through contests or challenges.
|
||||
|
||||
### Custom Campaign
|
||||
Custom campaign type with flexible configuration.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Audience Targeting**: Use segments and filters to target the right audience
|
||||
2. **Message Personalization**: Personalize messages for better engagement
|
||||
3. **Timing**: Schedule campaigns during peak engagement hours
|
||||
4. **A/B Testing**: Test different message variations
|
||||
5. **Budget Management**: Set realistic budgets and monitor spending
|
||||
6. **Compliance**: Ensure campaigns comply with regulations
|
||||
7. **Performance Monitoring**: Track metrics and adjust strategy
|
||||
|
||||
## Webhooks
|
||||
|
||||
You can configure webhooks to receive real-time updates about campaign events:
|
||||
|
||||
- `campaign.created`
|
||||
- `campaign.started`
|
||||
- `campaign.completed`
|
||||
- `campaign.failed`
|
||||
- `campaign.paused`
|
||||
- `campaign.resumed`
|
||||
|
||||
See [Webhooks API](./webhooks-api.md) for configuration details.
|
||||
600
marketing-agent/docs/api/examples.md
Normal file
600
marketing-agent/docs/api/examples.md
Normal file
@@ -0,0 +1,600 @@
|
||||
# API Usage Examples
|
||||
|
||||
Practical examples demonstrating common use cases for the Telegram Marketing Agent API.
|
||||
|
||||
## Complete Campaign Workflow
|
||||
|
||||
### 1. Authenticate
|
||||
|
||||
```bash
|
||||
# Login to get access token
|
||||
TOKEN=$(curl -s -X POST http://localhost:3000/api/v1/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "admin",
|
||||
"password": "password123"
|
||||
}' | jq -r '.data.tokens.accessToken')
|
||||
|
||||
echo "Token: $TOKEN"
|
||||
```
|
||||
|
||||
### 2. Create User Segment
|
||||
|
||||
```bash
|
||||
# Create a segment for active users
|
||||
SEGMENT_ID=$(curl -s -X POST http://localhost:3000/api/v1/segments \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Active Premium Users",
|
||||
"description": "Premium users active in last 7 days",
|
||||
"criteria": [
|
||||
{
|
||||
"field": "groups",
|
||||
"operator": "contains",
|
||||
"value": "Premium Users"
|
||||
},
|
||||
{
|
||||
"field": "engagement.lastActivity",
|
||||
"operator": "greater_than",
|
||||
"value": "7d"
|
||||
}
|
||||
],
|
||||
"logic": "AND"
|
||||
}' | jq -r '.data._id')
|
||||
|
||||
echo "Segment ID: $SEGMENT_ID"
|
||||
```
|
||||
|
||||
### 3. Create Message Template
|
||||
|
||||
```bash
|
||||
# Create a personalized message template
|
||||
TEMPLATE_ID=$(curl -s -X POST http://localhost:3000/api/v1/templates \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Premium Offer Template",
|
||||
"category": "promotional",
|
||||
"content": "Hi {{firstName}}! 🎉 As a valued premium member, enjoy 30% off your next purchase. Use code: PREMIUM30",
|
||||
"variables": [
|
||||
{
|
||||
"name": "firstName",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"defaultValue": "Valued Customer"
|
||||
}
|
||||
],
|
||||
"language": "en"
|
||||
}' | jq -r '.data.id')
|
||||
|
||||
echo "Template ID: $TEMPLATE_ID"
|
||||
```
|
||||
|
||||
### 4. Create Campaign
|
||||
|
||||
```bash
|
||||
# Create a marketing campaign
|
||||
CAMPAIGN_ID=$(curl -s -X POST http://localhost:3000/api/v1/campaigns \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Premium Member Exclusive Offer",
|
||||
"description": "30% discount for active premium members",
|
||||
"type": "message",
|
||||
"goals": {
|
||||
"targetAudience": 1000,
|
||||
"conversionRate": 15,
|
||||
"revenue": 50000
|
||||
},
|
||||
"targetAudience": {
|
||||
"segments": ["'$SEGMENT_ID'"]
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"templateId": "'$TEMPLATE_ID'",
|
||||
"personalization": {
|
||||
"enabled": true,
|
||||
"fields": ["firstName"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"budget": 5000
|
||||
}' | jq -r '.data.id')
|
||||
|
||||
echo "Campaign ID: $CAMPAIGN_ID"
|
||||
```
|
||||
|
||||
### 5. Schedule the Campaign
|
||||
|
||||
```bash
|
||||
# Schedule campaign to run daily at 10 AM
|
||||
curl -s -X POST http://localhost:3000/api/v1/scheduled-campaigns \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"campaignId": "'$CAMPAIGN_ID'",
|
||||
"campaignName": "Daily Premium Offers",
|
||||
"type": "recurring",
|
||||
"schedule": {
|
||||
"recurring": {
|
||||
"pattern": "daily",
|
||||
"timeOfDay": "10:00",
|
||||
"timezone": "America/New_York",
|
||||
"endDate": "2024-12-31T23:59:59Z"
|
||||
}
|
||||
},
|
||||
"targetAudience": {
|
||||
"type": "segment",
|
||||
"segmentId": "'$SEGMENT_ID'"
|
||||
},
|
||||
"messageConfig": {
|
||||
"templateId": "'$TEMPLATE_ID'"
|
||||
},
|
||||
"deliverySettings": {
|
||||
"priority": "normal",
|
||||
"rateLimiting": {
|
||||
"enabled": true,
|
||||
"messagesPerHour": 500
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### 6. Monitor Campaign Progress
|
||||
|
||||
```bash
|
||||
# Check campaign statistics
|
||||
curl -s -X GET "http://localhost:3000/api/v1/campaigns/$CAMPAIGN_ID/statistics" \
|
||||
-H "Authorization: Bearer $TOKEN" | jq '.data.overview'
|
||||
|
||||
# Get real-time progress
|
||||
curl -s -X GET "http://localhost:3000/api/v1/campaigns/$CAMPAIGN_ID/progress" \
|
||||
-H "Authorization: Bearer $TOKEN" | jq '.data.progress'
|
||||
```
|
||||
|
||||
## A/B Testing Example
|
||||
|
||||
### Create A/B Test
|
||||
|
||||
```bash
|
||||
# Create an A/B test for message variations
|
||||
AB_TEST_ID=$(curl -s -X POST http://localhost:3000/api/v1/experiments \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Emoji vs No Emoji Test",
|
||||
"description": "Test engagement with and without emojis",
|
||||
"type": "message",
|
||||
"status": "draft",
|
||||
"hypothesis": "Messages with emojis will have 20% higher engagement",
|
||||
"metrics": {
|
||||
"primary": "click_rate",
|
||||
"secondary": ["open_rate", "conversion_rate"]
|
||||
},
|
||||
"variants": [
|
||||
{
|
||||
"id": "control",
|
||||
"name": "No Emoji",
|
||||
"description": "Plain text message",
|
||||
"allocation": 50,
|
||||
"config": {
|
||||
"message": "Special offer: Get 25% off today!"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "variant_a",
|
||||
"name": "With Emoji",
|
||||
"description": "Message with emojis",
|
||||
"allocation": 50,
|
||||
"config": {
|
||||
"message": "🎉 Special offer: Get 25% off today! 🛍️"
|
||||
}
|
||||
}
|
||||
],
|
||||
"audience": {
|
||||
"segmentId": "'$SEGMENT_ID'",
|
||||
"size": 1000
|
||||
},
|
||||
"schedule": {
|
||||
"startDate": "2024-07-01T00:00:00Z",
|
||||
"endDate": "2024-07-07T23:59:59Z"
|
||||
}
|
||||
}' | jq -r '.data.id')
|
||||
|
||||
# Start the A/B test
|
||||
curl -s -X POST "http://localhost:3000/api/v1/experiments/$AB_TEST_ID/start" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## Workflow Automation Example
|
||||
|
||||
### Create Welcome Workflow
|
||||
|
||||
```bash
|
||||
# Create an automated welcome workflow
|
||||
WORKFLOW_ID=$(curl -s -X POST http://localhost:3000/api/v1/workflows \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "New User Welcome Series",
|
||||
"description": "3-step welcome series for new users",
|
||||
"trigger": {
|
||||
"type": "user_event",
|
||||
"event": "user_joined",
|
||||
"conditions": {
|
||||
"source": "telegram"
|
||||
}
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"id": "welcome_message",
|
||||
"type": "send_message",
|
||||
"name": "Welcome Message",
|
||||
"config": {
|
||||
"templateId": "welcome_template_1",
|
||||
"delay": {
|
||||
"value": 0,
|
||||
"unit": "minutes"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "wait_1_day",
|
||||
"type": "delay",
|
||||
"name": "Wait 1 Day",
|
||||
"config": {
|
||||
"duration": {
|
||||
"value": 1,
|
||||
"unit": "days"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tips_message",
|
||||
"type": "send_message",
|
||||
"name": "Tips Message",
|
||||
"config": {
|
||||
"templateId": "tips_template"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "check_engagement",
|
||||
"type": "condition",
|
||||
"name": "Check Engagement",
|
||||
"config": {
|
||||
"condition": {
|
||||
"field": "engagement.messagesSent",
|
||||
"operator": "greater_than",
|
||||
"value": 0
|
||||
},
|
||||
"trueBranch": "engaged_user_path",
|
||||
"falseBranch": "re_engagement_path"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "welcome_message",
|
||||
"target": "wait_1_day"
|
||||
},
|
||||
{
|
||||
"source": "wait_1_day",
|
||||
"target": "tips_message"
|
||||
},
|
||||
{
|
||||
"source": "tips_message",
|
||||
"target": "check_engagement"
|
||||
}
|
||||
]
|
||||
}' | jq -r '.data.id')
|
||||
|
||||
# Activate the workflow
|
||||
curl -s -X POST "http://localhost:3000/api/v1/workflows/$WORKFLOW_ID/activate" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## User Management Example
|
||||
|
||||
### Import Users from CSV
|
||||
|
||||
```bash
|
||||
# First, create a CSV file
|
||||
cat > users.csv << EOF
|
||||
phone,firstName,lastName,tags,groups
|
||||
+1234567890,John,Doe,"VIP,Newsletter","Premium Users"
|
||||
+0987654321,Jane,Smith,Newsletter,"Standard Users"
|
||||
+1122334455,Bob,Johnson,"VIP,Beta","Premium Users,Beta Testers"
|
||||
EOF
|
||||
|
||||
# Import users
|
||||
curl -X POST http://localhost:3000/api/v1/users/import \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-F "file=@users.csv" \
|
||||
-F 'mapping={
|
||||
"phone": "phone",
|
||||
"firstName": "firstName",
|
||||
"lastName": "lastName",
|
||||
"tags": "tags",
|
||||
"groups": "groups"
|
||||
}'
|
||||
```
|
||||
|
||||
### Bulk Tag Users
|
||||
|
||||
```bash
|
||||
# Tag all users who made a purchase
|
||||
curl -X POST http://localhost:3000/api/v1/users/bulk-update \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"filter": {
|
||||
"attributes.hasPurchased": true
|
||||
},
|
||||
"updates": {
|
||||
"addTags": ["Customer"],
|
||||
"attributes": {
|
||||
"customerSince": "2024-06-20"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Analytics Example
|
||||
|
||||
### Get Real-time Dashboard Data
|
||||
|
||||
```bash
|
||||
# Get current metrics
|
||||
curl -s -X GET http://localhost:3000/api/v1/analytics/realtime/metrics \
|
||||
-H "Authorization: Bearer $TOKEN" | jq '.data'
|
||||
|
||||
# Get campaign performance
|
||||
curl -s -X GET "http://localhost:3000/api/v1/analytics/campaigns?period=7d" \
|
||||
-H "Authorization: Bearer $TOKEN" | jq '.data.campaigns[0]'
|
||||
```
|
||||
|
||||
### Generate Custom Report
|
||||
|
||||
```bash
|
||||
# Create a custom report
|
||||
REPORT_ID=$(curl -s -X POST http://localhost:3000/api/v1/analytics/reports \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Weekly Performance Report",
|
||||
"type": "performance",
|
||||
"period": {
|
||||
"start": "2024-06-01T00:00:00Z",
|
||||
"end": "2024-06-30T23:59:59Z"
|
||||
},
|
||||
"metrics": [
|
||||
"messages_sent",
|
||||
"delivery_rate",
|
||||
"engagement_rate",
|
||||
"conversion_rate"
|
||||
],
|
||||
"groupBy": "campaign",
|
||||
"format": "pdf"
|
||||
}' | jq -r '.data.id')
|
||||
|
||||
# Download the report
|
||||
curl -X GET "http://localhost:3000/api/v1/analytics/reports/$REPORT_ID/download" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-o "weekly_report.pdf"
|
||||
```
|
||||
|
||||
## Webhook Integration Example
|
||||
|
||||
### Setup Webhook for Campaign Events
|
||||
|
||||
```bash
|
||||
# Create webhook endpoint
|
||||
WEBHOOK_ID=$(curl -s -X POST http://localhost:3000/api/v1/webhooks \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Campaign Status Webhook",
|
||||
"url": "https://your-server.com/webhooks/campaigns",
|
||||
"events": [
|
||||
"campaign.started",
|
||||
"campaign.completed",
|
||||
"campaign.failed"
|
||||
],
|
||||
"active": true,
|
||||
"headers": {
|
||||
"X-Custom-Header": "your-secret-value"
|
||||
},
|
||||
"retryPolicy": {
|
||||
"maxRetries": 3,
|
||||
"retryDelay": 60
|
||||
}
|
||||
}' | jq -r '.data.id')
|
||||
|
||||
# Test the webhook
|
||||
curl -X POST "http://localhost:3000/api/v1/webhooks/$WEBHOOK_ID/test" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
## Error Handling Examples
|
||||
|
||||
### Handle Rate Limiting
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Script with retry logic for rate limits
|
||||
|
||||
function api_call_with_retry() {
|
||||
local url=$1
|
||||
local max_retries=3
|
||||
local retry_count=0
|
||||
|
||||
while [ $retry_count -lt $max_retries ]; do
|
||||
response=$(curl -s -w "\n%{http_code}" -X GET "$url" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [ "$http_code" -eq 200 ]; then
|
||||
echo "$body"
|
||||
return 0
|
||||
elif [ "$http_code" -eq 429 ]; then
|
||||
retry_after=$(echo "$body" | jq -r '.retryAfter // 60')
|
||||
echo "Rate limited. Retrying after $retry_after seconds..." >&2
|
||||
sleep "$retry_after"
|
||||
((retry_count++))
|
||||
else
|
||||
echo "Error: HTTP $http_code" >&2
|
||||
echo "$body" >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Max retries exceeded" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# Use the function
|
||||
api_call_with_retry "http://localhost:3000/api/v1/campaigns"
|
||||
```
|
||||
|
||||
### Handle Pagination
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Script to fetch all users with pagination
|
||||
|
||||
function fetch_all_users() {
|
||||
local page=1
|
||||
local limit=100
|
||||
local total_fetched=0
|
||||
|
||||
while true; do
|
||||
response=$(curl -s -X GET "http://localhost:3000/api/v1/users?page=$page&limit=$limit" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
|
||||
users=$(echo "$response" | jq -r '.data.users')
|
||||
pagination=$(echo "$response" | jq -r '.data.pagination')
|
||||
|
||||
# Process users
|
||||
echo "$users" | jq -c '.[]' | while read -r user; do
|
||||
# Process each user
|
||||
echo "Processing user: $(echo "$user" | jq -r '.phone')"
|
||||
done
|
||||
|
||||
# Check if there are more pages
|
||||
current_page=$(echo "$pagination" | jq -r '.page')
|
||||
total_pages=$(echo "$pagination" | jq -r '.pages')
|
||||
|
||||
if [ "$current_page" -ge "$total_pages" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
((page++))
|
||||
done
|
||||
}
|
||||
|
||||
fetch_all_users
|
||||
```
|
||||
|
||||
## SDK Examples
|
||||
|
||||
### Node.js Example
|
||||
|
||||
```javascript
|
||||
const MarketingAPI = require('telegram-marketing-sdk');
|
||||
|
||||
const client = new MarketingAPI({
|
||||
baseURL: 'http://localhost:3000/api/v1',
|
||||
auth: {
|
||||
username: 'admin',
|
||||
password: 'password123'
|
||||
}
|
||||
});
|
||||
|
||||
// Create and execute a campaign
|
||||
async function runCampaign() {
|
||||
try {
|
||||
// Create campaign
|
||||
const campaign = await client.campaigns.create({
|
||||
name: 'Flash Sale',
|
||||
type: 'message',
|
||||
targetAudience: {
|
||||
segments: ['segment123']
|
||||
},
|
||||
messages: [{
|
||||
templateId: 'template456'
|
||||
}]
|
||||
});
|
||||
|
||||
// Execute campaign
|
||||
const execution = await client.campaigns.execute(campaign.id);
|
||||
|
||||
// Monitor progress
|
||||
const interval = setInterval(async () => {
|
||||
const progress = await client.campaigns.getProgress(campaign.id);
|
||||
console.log(`Progress: ${progress.percentage}%`);
|
||||
|
||||
if (progress.status === 'completed') {
|
||||
clearInterval(interval);
|
||||
console.log('Campaign completed!');
|
||||
}
|
||||
}, 5000);
|
||||
} catch (error) {
|
||||
console.error('Campaign failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
runCampaign();
|
||||
```
|
||||
|
||||
### Python Example
|
||||
|
||||
```python
|
||||
import telegram_marketing_sdk as tg
|
||||
import time
|
||||
|
||||
# Initialize client
|
||||
client = tg.Client(
|
||||
base_url='http://localhost:3000/api/v1',
|
||||
username='admin',
|
||||
password='password123'
|
||||
)
|
||||
|
||||
# Create user segment
|
||||
segment = client.segments.create(
|
||||
name='Python SDK Users',
|
||||
criteria=[
|
||||
{
|
||||
'field': 'attributes.sdk',
|
||||
'operator': 'equals',
|
||||
'value': 'python'
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
# Create and schedule campaign
|
||||
campaign = client.campaigns.create(
|
||||
name='Python SDK Campaign',
|
||||
type='message',
|
||||
target_audience={
|
||||
'segments': [segment.id]
|
||||
}
|
||||
)
|
||||
|
||||
schedule = client.scheduled_campaigns.create(
|
||||
campaign_id=campaign.id,
|
||||
campaign_name=f'{campaign.name} - Daily',
|
||||
type='recurring',
|
||||
schedule={
|
||||
'recurring': {
|
||||
'pattern': 'daily',
|
||||
'time_of_day': '10:00',
|
||||
'timezone': 'UTC'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
print(f'Campaign scheduled: {schedule.id}')
|
||||
```
|
||||
531
marketing-agent/docs/api/scheduled-campaigns-api.md
Normal file
531
marketing-agent/docs/api/scheduled-campaigns-api.md
Normal file
@@ -0,0 +1,531 @@
|
||||
# Scheduled Campaigns API
|
||||
|
||||
The Scheduled Campaigns API allows you to create and manage recurring or scheduled marketing campaigns.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### List Scheduled Campaigns
|
||||
|
||||
Get a list of scheduled campaigns.
|
||||
|
||||
```http
|
||||
GET /api/v1/scheduled-campaigns
|
||||
```
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Parameter | Type | Description | Default |
|
||||
|-----------|------|-------------|---------|
|
||||
| status | string | Filter by status (draft, scheduled, active, paused, completed, cancelled, failed) | - |
|
||||
| type | string | Filter by type (one-time, recurring, trigger-based) | - |
|
||||
| limit | integer | Items per page (max 100) | 100 |
|
||||
| skip | integer | Number of items to skip | 0 |
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"_id": "sched123",
|
||||
"campaignId": "camp123",
|
||||
"campaignName": "Weekly Newsletter",
|
||||
"type": "recurring",
|
||||
"schedule": {
|
||||
"recurring": {
|
||||
"pattern": "weekly",
|
||||
"daysOfWeek": [1],
|
||||
"timeOfDay": "10:00",
|
||||
"timezone": "America/New_York",
|
||||
"endDate": null,
|
||||
"maxOccurrences": null
|
||||
}
|
||||
},
|
||||
"targetAudience": {
|
||||
"type": "segment",
|
||||
"segmentId": "seg123"
|
||||
},
|
||||
"status": "active",
|
||||
"execution": {
|
||||
"nextRunAt": "2024-06-24T14:00:00Z",
|
||||
"lastRunAt": "2024-06-17T14:00:00Z",
|
||||
"runCount": 12
|
||||
},
|
||||
"statistics": {
|
||||
"totalRuns": 12,
|
||||
"totalMessagesSent": 24000,
|
||||
"avgDeliveryRate": 95.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Get Scheduled Campaign
|
||||
|
||||
Get a specific scheduled campaign.
|
||||
|
||||
```http
|
||||
GET /api/v1/scheduled-campaigns/:id
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"_id": "sched123",
|
||||
"campaignId": "camp123",
|
||||
"campaignName": "Weekly Newsletter",
|
||||
"type": "recurring",
|
||||
"schedule": {
|
||||
"recurring": {
|
||||
"pattern": "weekly",
|
||||
"daysOfWeek": [1],
|
||||
"timeOfDay": "10:00",
|
||||
"timezone": "America/New_York"
|
||||
}
|
||||
},
|
||||
"targetAudience": {
|
||||
"type": "segment",
|
||||
"segmentId": "seg123"
|
||||
},
|
||||
"messageConfig": {
|
||||
"templateId": "tmpl123",
|
||||
"personalization": {
|
||||
"enabled": true,
|
||||
"fields": ["firstName", "preferences"]
|
||||
}
|
||||
},
|
||||
"deliverySettings": {
|
||||
"priority": "normal",
|
||||
"rateLimiting": {
|
||||
"enabled": true,
|
||||
"messagesPerHour": 1000
|
||||
},
|
||||
"quietHours": {
|
||||
"enabled": true,
|
||||
"start": "22:00",
|
||||
"end": "08:00",
|
||||
"timezone": "UTC"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"onComplete": {
|
||||
"enabled": true,
|
||||
"channels": ["email"],
|
||||
"recipients": ["admin@example.com"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Create Scheduled Campaign
|
||||
|
||||
Create a new scheduled campaign.
|
||||
|
||||
```http
|
||||
POST /api/v1/scheduled-campaigns
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
##### One-Time Campaign
|
||||
|
||||
```json
|
||||
{
|
||||
"campaignId": "camp456",
|
||||
"campaignName": "Holiday Special",
|
||||
"type": "one-time",
|
||||
"schedule": {
|
||||
"startDateTime": "2024-12-25T10:00:00Z"
|
||||
},
|
||||
"targetAudience": {
|
||||
"type": "all"
|
||||
},
|
||||
"messageConfig": {
|
||||
"templateId": "tmpl789"
|
||||
},
|
||||
"deliverySettings": {
|
||||
"priority": "high"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Recurring Campaign
|
||||
|
||||
```json
|
||||
{
|
||||
"campaignId": "camp789",
|
||||
"campaignName": "Daily Tips",
|
||||
"type": "recurring",
|
||||
"schedule": {
|
||||
"recurring": {
|
||||
"pattern": "daily",
|
||||
"timeOfDay": "09:00",
|
||||
"timezone": "UTC",
|
||||
"endDate": "2024-12-31T23:59:59Z"
|
||||
}
|
||||
},
|
||||
"targetAudience": {
|
||||
"type": "segment",
|
||||
"segmentId": "seg456"
|
||||
},
|
||||
"messageConfig": {
|
||||
"templateId": "tmpl101",
|
||||
"variations": [
|
||||
{
|
||||
"name": "Morning",
|
||||
"templateId": "tmpl102",
|
||||
"weight": 50
|
||||
},
|
||||
{
|
||||
"name": "Standard",
|
||||
"templateId": "tmpl103",
|
||||
"weight": 50
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Trigger-Based Campaign
|
||||
|
||||
```json
|
||||
{
|
||||
"campaignId": "camp111",
|
||||
"campaignName": "Welcome Series",
|
||||
"type": "trigger-based",
|
||||
"schedule": {
|
||||
"triggers": [
|
||||
{
|
||||
"type": "user_event",
|
||||
"conditions": {
|
||||
"event": "user_registered",
|
||||
"source": "website"
|
||||
},
|
||||
"delay": {
|
||||
"value": 1,
|
||||
"unit": "hours"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"targetAudience": {
|
||||
"type": "dynamic",
|
||||
"dynamicCriteria": {
|
||||
"newUsers": true,
|
||||
"within": "24h"
|
||||
}
|
||||
},
|
||||
"messageConfig": {
|
||||
"templateId": "tmpl-welcome"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"_id": "sched456",
|
||||
"campaignName": "Holiday Special",
|
||||
"status": "scheduled",
|
||||
"execution": {
|
||||
"nextRunAt": "2024-12-25T10:00:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Update Scheduled Campaign
|
||||
|
||||
Update an existing scheduled campaign.
|
||||
|
||||
```http
|
||||
PUT /api/v1/scheduled-campaigns/:id
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"schedule": {
|
||||
"recurring": {
|
||||
"pattern": "weekly",
|
||||
"daysOfWeek": [1, 3, 5],
|
||||
"timeOfDay": "11:00"
|
||||
}
|
||||
},
|
||||
"deliverySettings": {
|
||||
"rateLimiting": {
|
||||
"enabled": true,
|
||||
"messagesPerHour": 2000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Delete Scheduled Campaign
|
||||
|
||||
Delete a scheduled campaign.
|
||||
|
||||
```http
|
||||
DELETE /api/v1/scheduled-campaigns/:id
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pause Scheduled Campaign
|
||||
|
||||
Pause an active scheduled campaign.
|
||||
|
||||
```http
|
||||
POST /api/v1/scheduled-campaigns/:id/pause
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"_id": "sched123",
|
||||
"status": "paused",
|
||||
"pausedAt": "2024-06-20T15:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Resume Scheduled Campaign
|
||||
|
||||
Resume a paused scheduled campaign.
|
||||
|
||||
```http
|
||||
POST /api/v1/scheduled-campaigns/:id/resume
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"_id": "sched123",
|
||||
"status": "active",
|
||||
"execution": {
|
||||
"nextRunAt": "2024-06-24T14:00:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Test Scheduled Campaign
|
||||
|
||||
Run a test execution of the scheduled campaign.
|
||||
|
||||
```http
|
||||
POST /api/v1/scheduled-campaigns/:id/test
|
||||
```
|
||||
|
||||
#### Request Body (Optional)
|
||||
|
||||
```json
|
||||
{
|
||||
"targetAudience": {
|
||||
"type": "individual",
|
||||
"userIds": ["user123", "user456"]
|
||||
},
|
||||
"maxRecipients": 10
|
||||
}
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"testJobId": "job123",
|
||||
"queueJobId": "queue456",
|
||||
"message": "Test job created and queued"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get Campaign History
|
||||
|
||||
Get execution history for a scheduled campaign.
|
||||
|
||||
```http
|
||||
GET /api/v1/scheduled-campaigns/:id/history
|
||||
```
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Parameter | Type | Description | Default |
|
||||
|-----------|------|-------------|---------|
|
||||
| limit | integer | Number of executions to return | 50 |
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"campaign": {
|
||||
"id": "sched123",
|
||||
"name": "Weekly Newsletter",
|
||||
"type": "recurring",
|
||||
"status": "active"
|
||||
},
|
||||
"executions": [
|
||||
{
|
||||
"runAt": "2024-06-17T14:00:00Z",
|
||||
"status": "success",
|
||||
"messagesSent": 2000,
|
||||
"errors": 0,
|
||||
"duration": 3600
|
||||
},
|
||||
{
|
||||
"runAt": "2024-06-10T14:00:00Z",
|
||||
"status": "success",
|
||||
"messagesSent": 1950,
|
||||
"errors": 5,
|
||||
"duration": 3450
|
||||
}
|
||||
],
|
||||
"jobs": [
|
||||
{
|
||||
"_id": "job789",
|
||||
"status": "completed",
|
||||
"scheduledFor": "2024-06-17T14:00:00Z",
|
||||
"completedAt": "2024-06-17T15:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get Campaign Statistics
|
||||
|
||||
Get aggregated statistics for scheduled campaigns.
|
||||
|
||||
```http
|
||||
GET /api/v1/scheduled-campaigns/statistics/:period
|
||||
```
|
||||
|
||||
#### Path Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| period | string | Time period (1h, 24h, 7d, 30d, 90d, 1y) |
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"totalCampaigns": 15,
|
||||
"activeCampaigns": 8,
|
||||
"totalRuns": 248,
|
||||
"totalMessagesSent": 124000,
|
||||
"totalDelivered": 118000,
|
||||
"avgDeliveryRate": 95.2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Schedule Patterns
|
||||
|
||||
### Daily Pattern
|
||||
|
||||
```json
|
||||
{
|
||||
"pattern": "daily",
|
||||
"timeOfDay": "10:00",
|
||||
"timezone": "America/New_York"
|
||||
}
|
||||
```
|
||||
|
||||
### Weekly Pattern
|
||||
|
||||
```json
|
||||
{
|
||||
"pattern": "weekly",
|
||||
"daysOfWeek": [1, 3, 5], // Monday, Wednesday, Friday
|
||||
"timeOfDay": "14:00",
|
||||
"timezone": "Europe/London"
|
||||
}
|
||||
```
|
||||
|
||||
### Monthly Pattern
|
||||
|
||||
```json
|
||||
{
|
||||
"pattern": "monthly",
|
||||
"daysOfMonth": [1, 15], // 1st and 15th of each month
|
||||
"timeOfDay": "09:00",
|
||||
"timezone": "Asia/Tokyo"
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Pattern
|
||||
|
||||
```json
|
||||
{
|
||||
"pattern": "custom",
|
||||
"frequency": {
|
||||
"interval": 2,
|
||||
"unit": "hours"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Timezones
|
||||
|
||||
The API supports all IANA timezone identifiers. Common examples:
|
||||
|
||||
- `UTC`
|
||||
- `America/New_York`
|
||||
- `America/Los_Angeles`
|
||||
- `Europe/London`
|
||||
- `Europe/Paris`
|
||||
- `Asia/Shanghai`
|
||||
- `Asia/Tokyo`
|
||||
- `Australia/Sydney`
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Timezone Awareness**: Always specify timezone for recurring campaigns
|
||||
2. **Rate Limiting**: Configure appropriate rate limits to avoid overwhelming users
|
||||
3. **Quiet Hours**: Respect user preferences with quiet hours configuration
|
||||
4. **Testing**: Test campaigns before setting them to active
|
||||
5. **Monitoring**: Set up notifications for campaign completion and failures
|
||||
6. **End Dates**: Set end dates for recurring campaigns to prevent indefinite runs
|
||||
7. **Error Handling**: Monitor execution history for failed runs
|
||||
|
||||
## Error Codes
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| INVALID_SCHEDULE | Schedule configuration is invalid |
|
||||
| CAMPAIGN_NOT_FOUND | Referenced campaign does not exist |
|
||||
| SCHEDULE_CONFLICT | Schedule conflicts with existing campaign |
|
||||
| PAST_DATE | Start date/time is in the past |
|
||||
| INVALID_TIMEZONE | Timezone identifier is not valid |
|
||||
@@ -0,0 +1,889 @@
|
||||
{
|
||||
"info": {
|
||||
"name": "Telegram Marketing Agent API",
|
||||
"description": "Comprehensive API collection for the Telegram Marketing Agent System. This collection includes all endpoints for managing campaigns, users, analytics, and more.\n\n## Getting Started\n\n1. Set the `base_url` variable to your API endpoint (default: http://localhost:3000)\n2. Authenticate using the Login endpoint to get your access token\n3. The token will be automatically saved to the `access_token` variable\n4. Start exploring the API!\n\n## Authentication\n\nMost endpoints require Bearer token authentication. The token is automatically managed after login.",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
"key": "base_url",
|
||||
"value": "http://localhost:3000",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "access_token",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"key": "refresh_token",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"bearer": [
|
||||
{
|
||||
"key": "token",
|
||||
"value": "{{access_token}}",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "Authentication",
|
||||
"item": [
|
||||
{
|
||||
"name": "Login",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"if (pm.response.code === 200) {",
|
||||
" const response = pm.response.json();",
|
||||
" pm.collectionVariables.set('access_token', response.data.accessToken);",
|
||||
" pm.collectionVariables.set('refresh_token', response.data.refreshToken);",
|
||||
" pm.environment.set('userId', response.data.user.id);",
|
||||
"}"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"username\": \"admin\",\n \"password\": \"password123\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/auth/login",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "auth", "login"]
|
||||
},
|
||||
"description": "Authenticate user and receive access token"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Register",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"username\": \"newuser\",\n \"email\": \"newuser@example.com\",\n \"password\": \"SecurePassword123!\",\n \"fullName\": \"John Doe\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/auth/register",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "auth", "register"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Refresh Token",
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"if (pm.response.code === 200) {",
|
||||
" const response = pm.response.json();",
|
||||
" pm.collectionVariables.set('access_token', response.data.accessToken);",
|
||||
"}"
|
||||
],
|
||||
"type": "text/javascript"
|
||||
}
|
||||
}
|
||||
],
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"refreshToken\": \"{{refresh_token}}\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/auth/refresh",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "auth", "refresh"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get Current User",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/auth/me",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "auth", "me"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Logout",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/auth/logout",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "auth", "logout"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Campaigns",
|
||||
"item": [
|
||||
{
|
||||
"name": "List Campaigns",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/orchestrator/campaigns?page=1&limit=20&status=active",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "orchestrator", "campaigns"],
|
||||
"query": [
|
||||
{
|
||||
"key": "page",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "20"
|
||||
},
|
||||
{
|
||||
"key": "status",
|
||||
"value": "active"
|
||||
},
|
||||
{
|
||||
"key": "type",
|
||||
"value": "message",
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Create Campaign",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"Summer Sale Campaign\",\n \"description\": \"Promotional campaign for summer products\",\n \"type\": \"message\",\n \"content\": {\n \"customMessage\": \"🌞 Summer Sale! Get 20% off on all products. Use code: SUMMER20\",\n \"media\": []\n },\n \"targeting\": {\n \"segments\": [],\n \"tags\": [\"customer\", \"active\"],\n \"filters\": {\n \"lastActivity\": {\n \"operator\": \"greater_than\",\n \"value\": \"30d\"\n }\n }\n },\n \"settings\": {\n \"rateLimit\": {\n \"messagesPerSecond\": 10,\n \"messagesPerUser\": 1\n }\n },\n \"goals\": {\n \"targetAudience\": 1000,\n \"conversionRate\": 15,\n \"revenue\": 50000\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/orchestrator/campaigns",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "orchestrator", "campaigns"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get Campaign",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/orchestrator/campaigns/:campaignId",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "orchestrator", "campaigns", ":campaignId"],
|
||||
"variable": [
|
||||
{
|
||||
"key": "campaignId",
|
||||
"value": "camp_123456789"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Update Campaign",
|
||||
"request": {
|
||||
"method": "PUT",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"Updated Summer Sale Campaign\",\n \"content\": {\n \"customMessage\": \"🌞 Extended Summer Sale! Get 25% off on all products. Use code: SUMMER25\"\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/orchestrator/campaigns/:campaignId",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "orchestrator", "campaigns", ":campaignId"],
|
||||
"variable": [
|
||||
{
|
||||
"key": "campaignId",
|
||||
"value": "camp_123456789"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Execute Campaign",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"test\": false\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/orchestrator/campaigns/:campaignId/execute",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "orchestrator", "campaigns", ":campaignId", "execute"],
|
||||
"variable": [
|
||||
{
|
||||
"key": "campaignId",
|
||||
"value": "camp_123456789"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get Campaign Statistics",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/orchestrator/campaigns/:campaignId/statistics?dateRange=last7days",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "orchestrator", "campaigns", ":campaignId", "statistics"],
|
||||
"query": [
|
||||
{
|
||||
"key": "dateRange",
|
||||
"value": "last7days"
|
||||
}
|
||||
],
|
||||
"variable": [
|
||||
{
|
||||
"key": "campaignId",
|
||||
"value": "camp_123456789"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Delete Campaign",
|
||||
"request": {
|
||||
"method": "DELETE",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/orchestrator/campaigns/:campaignId",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "orchestrator", "campaigns", ":campaignId"],
|
||||
"variable": [
|
||||
{
|
||||
"key": "campaignId",
|
||||
"value": "camp_123456789"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Scheduled Campaigns",
|
||||
"item": [
|
||||
{
|
||||
"name": "List Scheduled Campaigns",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/scheduler/scheduled-campaigns?status=active&type=recurring",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "scheduler", "scheduled-campaigns"],
|
||||
"query": [
|
||||
{
|
||||
"key": "status",
|
||||
"value": "active"
|
||||
},
|
||||
{
|
||||
"key": "type",
|
||||
"value": "recurring"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Create Schedule",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"campaignId\": \"camp_123456789\",\n \"name\": \"Daily Morning Newsletter\",\n \"description\": \"Send newsletter every morning at 9 AM\",\n \"type\": \"recurring\",\n \"schedule\": {\n \"startDateTime\": \"2024-01-15T09:00:00Z\",\n \"recurring\": {\n \"pattern\": \"daily\",\n \"frequency\": {\n \"interval\": 1,\n \"unit\": \"day\"\n },\n \"time\": \"09:00\",\n \"timezone\": \"America/New_York\"\n }\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/scheduler/scheduled-campaigns",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "scheduler", "scheduled-campaigns"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Pause Schedule",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/scheduler/scheduled-campaigns/:scheduleId/pause",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "scheduler", "scheduled-campaigns", ":scheduleId", "pause"],
|
||||
"variable": [
|
||||
{
|
||||
"key": "scheduleId",
|
||||
"value": "sched_123456789"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Resume Schedule",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/scheduler/scheduled-campaigns/:scheduleId/resume",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "scheduler", "scheduled-campaigns", ":scheduleId", "resume"],
|
||||
"variable": [
|
||||
{
|
||||
"key": "scheduleId",
|
||||
"value": "sched_123456789"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Users",
|
||||
"item": [
|
||||
{
|
||||
"name": "List Users",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/users?page=1&limit=20&status=active",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "users"],
|
||||
"query": [
|
||||
{
|
||||
"key": "page",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"key": "limit",
|
||||
"value": "20"
|
||||
},
|
||||
{
|
||||
"key": "status",
|
||||
"value": "active"
|
||||
},
|
||||
{
|
||||
"key": "search",
|
||||
"value": "john",
|
||||
"disabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Create User",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"telegramId\": \"123456789\",\n \"username\": \"johndoe\",\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"phoneNumber\": \"+1234567890\",\n \"languageCode\": \"en\",\n \"tags\": [\"customer\", \"active\"],\n \"customFields\": {\n \"company\": \"Acme Corp\",\n \"subscription\": \"premium\"\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/users",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "users"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Import Users",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "formdata",
|
||||
"formdata": [
|
||||
{
|
||||
"key": "file",
|
||||
"type": "file",
|
||||
"src": "/path/to/users.csv"
|
||||
},
|
||||
{
|
||||
"key": "updateExisting",
|
||||
"value": "true",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "defaultTags",
|
||||
"value": "[\"imported\", \"newsletter\"]",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/users/import",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "users", "import"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Export Users",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"format\": \"csv\",\n \"filters\": {\n \"status\": \"active\",\n \"tags\": [\"customer\"]\n },\n \"fields\": [\"telegramId\", \"username\", \"firstName\", \"lastName\", \"tags\", \"createdAt\"]\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/users/export",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "users", "export"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Bulk Update Users",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"userIds\": [\"user_123\", \"user_456\", \"user_789\"],\n \"updates\": {\n \"addTags\": [\"vip\", \"2024-campaign\"],\n \"removeTags\": [\"inactive\"],\n \"customFields\": {\n \"lastCampaign\": \"Summer Sale 2024\"\n }\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/users/bulk",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "users", "bulk"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Segments",
|
||||
"item": [
|
||||
{
|
||||
"name": "List Segments",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/segments",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "segments"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Create Segment",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"Active Premium Users\",\n \"description\": \"Users who are premium subscribers and active in the last 7 days\",\n \"criteria\": [\n {\n \"field\": \"tags\",\n \"operator\": \"contains\",\n \"value\": \"premium\",\n \"logic\": \"AND\"\n },\n {\n \"field\": \"engagement.lastActivity\",\n \"operator\": \"greater_than\",\n \"value\": \"7d\"\n }\n ],\n \"isDynamic\": true\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/segments",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "segments"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Preview Segment",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"criteria\": [\n {\n \"field\": \"tags\",\n \"operator\": \"contains\",\n \"value\": \"newsletter\"\n }\n ]\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/segments/preview",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "segments", "preview"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get Segment Users",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/segments/:segmentId/users",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "segments", ":segmentId", "users"],
|
||||
"variable": [
|
||||
{
|
||||
"key": "segmentId",
|
||||
"value": "seg_123456789"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Analytics",
|
||||
"item": [
|
||||
{
|
||||
"name": "Campaign Analytics",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/analytics/campaigns?dateRange=last30days",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "analytics", "campaigns"],
|
||||
"query": [
|
||||
{
|
||||
"key": "dateRange",
|
||||
"value": "last30days"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "User Analytics",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/analytics/users?metrics=growth,engagement",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "analytics", "users"],
|
||||
"query": [
|
||||
{
|
||||
"key": "metrics",
|
||||
"value": "growth,engagement"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Real-time Dashboard",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/analytics/dashboard",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "analytics", "dashboard"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Templates",
|
||||
"item": [
|
||||
{
|
||||
"name": "List Templates",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/templates?category=promotional",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "templates"],
|
||||
"query": [
|
||||
{
|
||||
"key": "category",
|
||||
"value": "promotional"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Create Template",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"Welcome Message\",\n \"category\": \"onboarding\",\n \"content\": {\n \"en\": \"Welcome {{firstName}}! 🎉 Thank you for joining us.\",\n \"es\": \"¡Bienvenido {{firstName}}! 🎉 Gracias por unirte a nosotros.\"\n },\n \"variables\": [\"firstName\"],\n \"isActive\": true\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/templates",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "templates"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Webhooks",
|
||||
"item": [
|
||||
{
|
||||
"name": "List Webhooks",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/webhooks",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "webhooks"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Create Webhook",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"Campaign Events\",\n \"url\": \"https://your-server.com/webhooks/campaigns\",\n \"events\": [\"campaign.completed\", \"campaign.failed\", \"user.converted\"],\n \"headers\": {\n \"X-Webhook-Secret\": \"your-secret-key\"\n },\n \"isActive\": true\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/webhooks",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "webhooks"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Test Webhook",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"event\": \"campaign.completed\",\n \"payload\": {\n \"campaignId\": \"camp_test_123\",\n \"name\": \"Test Campaign\"\n }\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/webhooks/:webhookId/test",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "webhooks", ":webhookId", "test"],
|
||||
"variable": [
|
||||
{
|
||||
"key": "webhookId",
|
||||
"value": "webhook_123456789"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "System",
|
||||
"item": [
|
||||
{
|
||||
"name": "Health Check",
|
||||
"request": {
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/health",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["health"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Service Health",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/health/services",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "health", "services"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Metrics",
|
||||
"request": {
|
||||
"auth": {
|
||||
"type": "noauth"
|
||||
},
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{base_url}}/metrics",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["metrics"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Create Backup",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"includeUsers\": true,\n \"includeCampaigns\": true,\n \"includeAnalytics\": true,\n \"compression\": true\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{base_url}}/api/v1/backup",
|
||||
"host": ["{{base_url}}"],
|
||||
"path": ["api", "v1", "backup"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"// Set default headers",
|
||||
"pm.request.headers.add({",
|
||||
" key: 'Content-Type',",
|
||||
" value: 'application/json'",
|
||||
"});",
|
||||
"",
|
||||
"// Add request ID",
|
||||
"pm.request.headers.add({",
|
||||
" key: 'X-Request-ID',",
|
||||
" value: pm.variables.replaceIn('{{$guid}}')",
|
||||
"});"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"// Log response time",
|
||||
"console.log(`Response time: ${pm.response.responseTime}ms`);",
|
||||
"",
|
||||
"// Check for common errors",
|
||||
"if (pm.response.code >= 400) {",
|
||||
" const jsonData = pm.response.json();",
|
||||
" console.error(`Error: ${jsonData.error || jsonData.message}`);",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
623
marketing-agent/docs/api/users-api.md
Normal file
623
marketing-agent/docs/api/users-api.md
Normal file
@@ -0,0 +1,623 @@
|
||||
# Users API
|
||||
|
||||
The Users API provides endpoints for managing users, groups, tags, and segments.
|
||||
|
||||
## User Endpoints
|
||||
|
||||
### List Users
|
||||
|
||||
Get a paginated list of users.
|
||||
|
||||
```http
|
||||
GET /api/v1/users
|
||||
```
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Parameter | Type | Description | Default |
|
||||
|-----------|------|-------------|---------|
|
||||
| page | integer | Page number | 1 |
|
||||
| limit | integer | Items per page (max 100) | 20 |
|
||||
| search | string | Search in phone, firstName, lastName | - |
|
||||
| status | string | Filter by status (active, inactive, blocked) | - |
|
||||
| groups | string | Comma-separated group IDs | - |
|
||||
| tags | string | Comma-separated tag IDs | - |
|
||||
| sort | string | Sort field (prefix with - for descending) | -createdAt |
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"users": [
|
||||
{
|
||||
"_id": "user123",
|
||||
"accountId": "acc123",
|
||||
"phone": "+1234567890",
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"username": "johndoe",
|
||||
"status": "active",
|
||||
"groups": [
|
||||
{
|
||||
"_id": "grp123",
|
||||
"name": "Premium Users"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"_id": "tag123",
|
||||
"name": "VIP",
|
||||
"color": "#FFD700"
|
||||
}
|
||||
],
|
||||
"attributes": {
|
||||
"location": "New York",
|
||||
"language": "en",
|
||||
"joinDate": "2024-01-15T10:00:00Z"
|
||||
},
|
||||
"engagement": {
|
||||
"lastActivity": "2024-06-20T15:30:00Z",
|
||||
"messagesSent": 45,
|
||||
"messagesReceived": 120,
|
||||
"engagementScore": 85
|
||||
},
|
||||
"createdAt": "2024-01-15T10:00:00Z",
|
||||
"updatedAt": "2024-06-20T15:30:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total": 1250,
|
||||
"pages": 63
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get User
|
||||
|
||||
Get a specific user by ID.
|
||||
|
||||
```http
|
||||
GET /api/v1/users/:id
|
||||
```
|
||||
|
||||
### Create User
|
||||
|
||||
Create a new user.
|
||||
|
||||
```http
|
||||
POST /api/v1/users
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"phone": "+1234567890",
|
||||
"firstName": "Jane",
|
||||
"lastName": "Smith",
|
||||
"username": "janesmith",
|
||||
"groups": ["grp123"],
|
||||
"tags": ["tag456"],
|
||||
"attributes": {
|
||||
"location": "Los Angeles",
|
||||
"language": "en",
|
||||
"source": "website"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Update User
|
||||
|
||||
Update user information.
|
||||
|
||||
```http
|
||||
PUT /api/v1/users/:id
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"firstName": "Jane",
|
||||
"lastName": "Johnson",
|
||||
"status": "active",
|
||||
"attributes": {
|
||||
"location": "San Francisco",
|
||||
"preferences": {
|
||||
"notifications": true,
|
||||
"newsletter": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Delete User
|
||||
|
||||
Delete a user.
|
||||
|
||||
```http
|
||||
DELETE /api/v1/users/:id
|
||||
```
|
||||
|
||||
### Bulk Update Users
|
||||
|
||||
Update multiple users at once.
|
||||
|
||||
```http
|
||||
POST /api/v1/users/bulk-update
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"userIds": ["user123", "user456", "user789"],
|
||||
"updates": {
|
||||
"addGroups": ["grp789"],
|
||||
"removeTags": ["tag123"],
|
||||
"attributes": {
|
||||
"campaign": "summer2024"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Import Users
|
||||
|
||||
Import users from CSV.
|
||||
|
||||
```http
|
||||
POST /api/v1/users/import
|
||||
```
|
||||
|
||||
#### Request
|
||||
|
||||
- Content-Type: multipart/form-data
|
||||
- File field name: `file`
|
||||
- Optional field: `mapping` (JSON string)
|
||||
|
||||
#### Example CSV Format
|
||||
|
||||
```csv
|
||||
phone,firstName,lastName,tags,groups
|
||||
+1234567890,John,Doe,VIP,"Premium Users"
|
||||
+0987654321,Jane,Smith,"VIP,Gold","Premium Users,Beta Testers"
|
||||
```
|
||||
|
||||
### Export Users
|
||||
|
||||
Export users to CSV.
|
||||
|
||||
```http
|
||||
GET /api/v1/users/export
|
||||
```
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| format | string | Export format (csv, json) |
|
||||
| fields | string | Comma-separated fields to export |
|
||||
| filters | object | Same as list filters |
|
||||
|
||||
## Group Endpoints
|
||||
|
||||
### List Groups
|
||||
|
||||
Get all user groups.
|
||||
|
||||
```http
|
||||
GET /api/v1/groups
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"_id": "grp123",
|
||||
"name": "Premium Users",
|
||||
"description": "Users with premium subscription",
|
||||
"type": "static",
|
||||
"memberCount": 450,
|
||||
"color": "#4CAF50",
|
||||
"isDefault": false,
|
||||
"createdAt": "2024-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"_id": "grp456",
|
||||
"name": "Active Last 7 Days",
|
||||
"description": "Users active in the last 7 days",
|
||||
"type": "dynamic",
|
||||
"memberCount": 832,
|
||||
"rules": {
|
||||
"criteria": [
|
||||
{
|
||||
"field": "engagement.lastActivity",
|
||||
"operator": "greater_than",
|
||||
"value": "7d"
|
||||
}
|
||||
],
|
||||
"logic": "AND"
|
||||
},
|
||||
"lastCalculated": "2024-06-20T15:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Create Group
|
||||
|
||||
Create a new user group.
|
||||
|
||||
```http
|
||||
POST /api/v1/groups
|
||||
```
|
||||
|
||||
#### Request Body (Static Group)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Beta Testers",
|
||||
"description": "Users testing beta features",
|
||||
"type": "static",
|
||||
"color": "#FF5722"
|
||||
}
|
||||
```
|
||||
|
||||
#### Request Body (Dynamic Group)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "High Engagement Users",
|
||||
"description": "Users with engagement score > 80",
|
||||
"type": "dynamic",
|
||||
"rules": {
|
||||
"criteria": [
|
||||
{
|
||||
"field": "engagement.engagementScore",
|
||||
"operator": "greater_than",
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"logic": "AND"
|
||||
},
|
||||
"color": "#9C27B0"
|
||||
}
|
||||
```
|
||||
|
||||
### Update Group
|
||||
|
||||
Update group information.
|
||||
|
||||
```http
|
||||
PUT /api/v1/groups/:id
|
||||
```
|
||||
|
||||
### Delete Group
|
||||
|
||||
Delete a group.
|
||||
|
||||
```http
|
||||
DELETE /api/v1/groups/:id
|
||||
```
|
||||
|
||||
### Add Users to Group
|
||||
|
||||
Add users to a static group.
|
||||
|
||||
```http
|
||||
POST /api/v1/groups/:id/add-users
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"userIds": ["user123", "user456", "user789"]
|
||||
}
|
||||
```
|
||||
|
||||
### Remove Users from Group
|
||||
|
||||
Remove users from a static group.
|
||||
|
||||
```http
|
||||
POST /api/v1/groups/:id/remove-users
|
||||
```
|
||||
|
||||
### Recalculate Dynamic Group
|
||||
|
||||
Manually trigger recalculation of a dynamic group.
|
||||
|
||||
```http
|
||||
POST /api/v1/groups/:id/recalculate
|
||||
```
|
||||
|
||||
## Tag Endpoints
|
||||
|
||||
### List Tags
|
||||
|
||||
Get all tags.
|
||||
|
||||
```http
|
||||
GET /api/v1/tags
|
||||
```
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| category | string | Filter by category |
|
||||
| search | string | Search in tag names |
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"_id": "tag123",
|
||||
"name": "VIP",
|
||||
"category": "status",
|
||||
"color": "#FFD700",
|
||||
"description": "Very important customers",
|
||||
"usageCount": 156,
|
||||
"isSystem": false,
|
||||
"createdAt": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Create Tag
|
||||
|
||||
Create a new tag.
|
||||
|
||||
```http
|
||||
POST /api/v1/tags
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Newsletter",
|
||||
"category": "preferences",
|
||||
"color": "#2196F3",
|
||||
"description": "Subscribed to newsletter"
|
||||
}
|
||||
```
|
||||
|
||||
### Update Tag
|
||||
|
||||
Update tag information.
|
||||
|
||||
```http
|
||||
PUT /api/v1/tags/:id
|
||||
```
|
||||
|
||||
### Delete Tag
|
||||
|
||||
Delete a tag.
|
||||
|
||||
```http
|
||||
DELETE /api/v1/tags/:id
|
||||
```
|
||||
|
||||
### Merge Tags
|
||||
|
||||
Merge multiple tags into one.
|
||||
|
||||
```http
|
||||
POST /api/v1/tags/merge
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceTagIds": ["tag456", "tag789"],
|
||||
"targetTagId": "tag123"
|
||||
}
|
||||
```
|
||||
|
||||
## Segment Endpoints
|
||||
|
||||
### List Segments
|
||||
|
||||
Get all segments.
|
||||
|
||||
```http
|
||||
GET /api/v1/segments
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"_id": "seg123",
|
||||
"name": "High Value Customers",
|
||||
"description": "Customers with high purchase value and engagement",
|
||||
"criteria": [
|
||||
{
|
||||
"field": "attributes.totalPurchases",
|
||||
"operator": "greater_than",
|
||||
"value": 1000
|
||||
},
|
||||
{
|
||||
"field": "engagement.engagementScore",
|
||||
"operator": "greater_than",
|
||||
"value": 70
|
||||
}
|
||||
],
|
||||
"logic": "AND",
|
||||
"userCount": 342,
|
||||
"lastCalculated": "2024-06-20T15:00:00Z",
|
||||
"isActive": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Create Segment
|
||||
|
||||
Create a new segment.
|
||||
|
||||
```http
|
||||
POST /api/v1/segments
|
||||
```
|
||||
|
||||
#### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Inactive Users",
|
||||
"description": "Users who haven't been active in 30 days",
|
||||
"criteria": [
|
||||
{
|
||||
"field": "engagement.lastActivity",
|
||||
"operator": "less_than",
|
||||
"value": "30d"
|
||||
}
|
||||
],
|
||||
"logic": "AND",
|
||||
"excludeSegments": ["seg456"]
|
||||
}
|
||||
```
|
||||
|
||||
### Test Segment
|
||||
|
||||
Test segment criteria and get user count.
|
||||
|
||||
```http
|
||||
POST /api/v1/segments/:id/test
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"userCount": 156,
|
||||
"sampleUsers": [
|
||||
{
|
||||
"_id": "user123",
|
||||
"phone": "+1234567890",
|
||||
"firstName": "John"
|
||||
}
|
||||
],
|
||||
"executionTime": 125
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Export Segment Users
|
||||
|
||||
Export users in a segment.
|
||||
|
||||
```http
|
||||
GET /api/v1/segments/:id/export
|
||||
```
|
||||
|
||||
### Clone Segment
|
||||
|
||||
Create a copy of a segment.
|
||||
|
||||
```http
|
||||
POST /api/v1/segments/:id/clone
|
||||
```
|
||||
|
||||
## Statistics Endpoints
|
||||
|
||||
### User Statistics
|
||||
|
||||
Get user statistics.
|
||||
|
||||
```http
|
||||
GET /api/v1/users-stats
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"totalUsers": 12500,
|
||||
"activeUsers": 8900,
|
||||
"newUsers": {
|
||||
"today": 45,
|
||||
"thisWeek": 312,
|
||||
"thisMonth": 1250
|
||||
},
|
||||
"engagement": {
|
||||
"avgEngagementScore": 72.5,
|
||||
"highlyEngaged": 3200,
|
||||
"moderatelyEngaged": 5700,
|
||||
"lowEngagement": 3600
|
||||
},
|
||||
"growth": {
|
||||
"daily": 2.5,
|
||||
"weekly": 8.3,
|
||||
"monthly": 15.6
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Group Statistics
|
||||
|
||||
Get group statistics.
|
||||
|
||||
```http
|
||||
GET /api/v1/groups-stats
|
||||
```
|
||||
|
||||
### Tag Statistics
|
||||
|
||||
Get tag usage statistics.
|
||||
|
||||
```http
|
||||
GET /api/v1/tags-stats
|
||||
```
|
||||
|
||||
## Supported Operators
|
||||
|
||||
For dynamic groups and segments:
|
||||
|
||||
| Operator | Description | Example |
|
||||
|----------|-------------|----------|
|
||||
| equals | Exact match | status equals "active" |
|
||||
| not_equals | Not equal | status not_equals "blocked" |
|
||||
| contains | Contains string | tags contains "VIP" |
|
||||
| not_contains | Doesn't contain | groups not_contains "Blocked" |
|
||||
| greater_than | Greater than | engagementScore greater_than 80 |
|
||||
| less_than | Less than | lastActivity less_than "7d" |
|
||||
| greater_or_equal | Greater or equal | totalPurchases greater_or_equal 100 |
|
||||
| less_or_equal | Less or equal | messagesSent less_or_equal 10 |
|
||||
| in | In list | location in ["NY", "CA", "TX"] |
|
||||
| not_in | Not in list | status not_in ["blocked", "inactive"] |
|
||||
| exists | Field exists | email exists true |
|
||||
| regex | Regular expression | phone regex "^\+1" |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Segmentation**: Use segments for targeted campaigns
|
||||
2. **Dynamic Groups**: Leverage dynamic groups for automatic user categorization
|
||||
3. **Tags**: Use tags for flexible user labeling
|
||||
4. **Bulk Operations**: Use bulk endpoints for efficiency
|
||||
5. **Caching**: Segment calculations are cached for performance
|
||||
6. **Regular Updates**: Keep user data updated for accurate targeting
|
||||
267
marketing-agent/docs/architecture/system-architecture.md
Normal file
267
marketing-agent/docs/architecture/system-architecture.md
Normal file
@@ -0,0 +1,267 @@
|
||||
# System Architecture - Marketing Intelligence Agent
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Admin Frontend │
|
||||
│ (React + TypeScript + Ant Design) │
|
||||
└─────────────────────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌─────────┴──────────┐
|
||||
│ API Gateway │
|
||||
│ (Nginx/Kong) │
|
||||
└─────────┬──────────┘
|
||||
│
|
||||
┌─────────────────────────────┼─────────────────────────────┐
|
||||
│ │ │
|
||||
┌───────┴────────┐ ┌────────┴────────┐ ┌────────┴────────┐
|
||||
│ Orchestrator │ │ Claude Agent │ │ gramJS Adapter │
|
||||
│ Service │◄─────────►│ Service │◄────────►│ Service │
|
||||
└───────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
||||
│ │ │
|
||||
│ ┌────────┴────────┐ │
|
||||
│ │ Safety Guard │ │
|
||||
│ │ Service │◄──────────────────┘
|
||||
│ └────────┬────────┘
|
||||
│ │
|
||||
┌───────┴────────┐ ┌────────┴────────┐ ┌─────────────────┐
|
||||
│ Analytics │ │ A/B Testing │ │ Compliance │
|
||||
│ Service │◄─────────►│ Service │◄────────►│ Guard │
|
||||
└────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │ │
|
||||
└─────────────────────────────┼─────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────┼──────────────────────┐
|
||||
│ │ │
|
||||
┌───────┴────────┐ ┌───────┴────────┐ ┌────────┴────────┐
|
||||
│ PostgreSQL │ │ MongoDB │ │ Redis │
|
||||
│ (Main DB) │ │ (Events) │ │ (Cache/Queue) │
|
||||
└────────────────┘ └────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
## Service Descriptions
|
||||
|
||||
### 1. API Gateway (Port 8000)
|
||||
- **Purpose**: Central entry point for all API requests
|
||||
- **Technology**: Nginx or Kong
|
||||
- **Features**:
|
||||
- Request routing
|
||||
- Authentication
|
||||
- Rate limiting
|
||||
- Load balancing
|
||||
- SSL termination
|
||||
|
||||
### 2. Orchestrator Service (Port 3001)
|
||||
- **Purpose**: Task scheduling and workflow management
|
||||
- **Key Components**:
|
||||
- Task Queue Manager (BullMQ)
|
||||
- State Machine Engine (XState)
|
||||
- Task Scheduler
|
||||
- Workflow Engine
|
||||
- **Database**: PostgreSQL for task persistence
|
||||
|
||||
### 3. Claude Agent Service (Port 3002)
|
||||
- **Purpose**: AI-powered decision making and content generation
|
||||
- **Key Components**:
|
||||
- Claude API Integration
|
||||
- Function Calling Handler
|
||||
- Prompt Template Engine
|
||||
- Context Manager
|
||||
- Token Usage Tracker
|
||||
- **Integration**: Anthropic Claude API
|
||||
|
||||
### 4. gramJS Adapter Service (Port 3003)
|
||||
- **Purpose**: Telegram automation and messaging
|
||||
- **Key Components**:
|
||||
- Connection Pool Manager
|
||||
- Session Manager
|
||||
- Message Tools (send, create, invite, etc.)
|
||||
- Event Listener
|
||||
- FloodWait Handler
|
||||
- **Integration**: Telegram API via gramJS
|
||||
|
||||
### 5. Safety Guard Service (Port 3004)
|
||||
- **Purpose**: Compliance and safety enforcement
|
||||
- **Key Components**:
|
||||
- Rate Limiter
|
||||
- Content Filter
|
||||
- Keyword Blacklist
|
||||
- PII Scanner
|
||||
- ToS Compliance Checker
|
||||
- Account Health Monitor
|
||||
|
||||
### 6. Analytics Service (Port 3005)
|
||||
- **Purpose**: Data collection and analysis
|
||||
- **Key Components**:
|
||||
- Event Collector
|
||||
- Real-time Aggregator
|
||||
- Funnel Analyzer
|
||||
- User Segmentation
|
||||
- ROI Calculator
|
||||
- **Database**: MongoDB for event storage
|
||||
|
||||
### 7. A/B Testing Service (Port 3006)
|
||||
- **Purpose**: Experiment management and optimization
|
||||
- **Key Components**:
|
||||
- Experiment Engine
|
||||
- Traffic Splitter
|
||||
- Multi-Armed Bandit
|
||||
- Bayesian Optimizer
|
||||
- Statistical Calculator
|
||||
|
||||
### 8. Compliance Guard Service (Port 3007)
|
||||
- **Purpose**: Legal compliance and data protection
|
||||
- **Key Components**:
|
||||
- GDPR Compliance
|
||||
- CCPA Compliance
|
||||
- Data Privacy Scanner
|
||||
- Audit Logger
|
||||
- Consent Manager
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Campaign Creation Flow
|
||||
```
|
||||
1. User → Admin UI → API Gateway
|
||||
2. API Gateway → Orchestrator Service
|
||||
3. Orchestrator → Claude Agent (Strategy Generation)
|
||||
4. Claude Agent → Safety Guard (Content Validation)
|
||||
5. Safety Guard → Orchestrator (Approved/Rejected)
|
||||
6. Orchestrator → Campaign Database
|
||||
```
|
||||
|
||||
### Message Sending Flow
|
||||
```
|
||||
1. Orchestrator → Task Queue
|
||||
2. Task Worker → gramJS Adapter
|
||||
3. gramJS Adapter → Safety Guard (Pre-check)
|
||||
4. Safety Guard → gramJS Adapter (Approved)
|
||||
5. gramJS Adapter → Telegram API
|
||||
6. Result → Analytics Service
|
||||
7. Analytics → Event Store
|
||||
```
|
||||
|
||||
### Human-in-the-Loop Flow
|
||||
```
|
||||
1. High-risk Task → Escalation Engine
|
||||
2. Escalation → Human Review Queue
|
||||
3. Admin UI → Review Interface
|
||||
4. Human Decision → Orchestrator
|
||||
5. Orchestrator → Continue/Abort Task
|
||||
```
|
||||
|
||||
## Infrastructure Components
|
||||
|
||||
### Message Queue (RabbitMQ)
|
||||
- Task distribution
|
||||
- Service communication
|
||||
- Event broadcasting
|
||||
- Dead letter queues
|
||||
|
||||
### Cache Layer (Redis)
|
||||
- Session storage
|
||||
- Rate limiting counters
|
||||
- Temporary data
|
||||
- Task queues (BullMQ)
|
||||
|
||||
### Search Engine (Elasticsearch)
|
||||
- Log aggregation
|
||||
- Full-text search
|
||||
- Analytics queries
|
||||
- Audit trail search
|
||||
|
||||
### Monitoring Stack
|
||||
- **Prometheus**: Metrics collection
|
||||
- **Grafana**: Visualization
|
||||
- **Jaeger**: Distributed tracing
|
||||
- **ELK Stack**: Log management
|
||||
|
||||
## Security Architecture
|
||||
|
||||
### Authentication & Authorization
|
||||
- JWT-based authentication
|
||||
- Role-based access control (RBAC)
|
||||
- API key management
|
||||
- OAuth2 integration
|
||||
|
||||
### Data Protection
|
||||
- End-to-end encryption
|
||||
- At-rest encryption
|
||||
- TLS/SSL for transit
|
||||
- Key rotation
|
||||
|
||||
### Network Security
|
||||
- VPC isolation
|
||||
- Security groups
|
||||
- WAF protection
|
||||
- DDoS mitigation
|
||||
|
||||
## Scalability Considerations
|
||||
|
||||
### Horizontal Scaling
|
||||
- Stateless services
|
||||
- Load balancer distribution
|
||||
- Database read replicas
|
||||
- Cache clustering
|
||||
|
||||
### Vertical Scaling
|
||||
- Resource monitoring
|
||||
- Auto-scaling groups
|
||||
- Performance tuning
|
||||
- Database optimization
|
||||
|
||||
### High Availability
|
||||
- Multi-AZ deployment
|
||||
- Failover mechanisms
|
||||
- Health checks
|
||||
- Backup strategies
|
||||
|
||||
## Deployment Architecture
|
||||
|
||||
### Container Orchestration
|
||||
```yaml
|
||||
Kubernetes Cluster:
|
||||
- Namespace: marketing-agent
|
||||
- Services: 8 microservices
|
||||
- Ingress: NGINX controller
|
||||
- Storage: PersistentVolumes
|
||||
- ConfigMaps: Environment configs
|
||||
- Secrets: Sensitive data
|
||||
```
|
||||
|
||||
### CI/CD Pipeline
|
||||
```
|
||||
1. Code Push → GitHub
|
||||
2. GitHub Actions → Build & Test
|
||||
3. Docker Build → Container Registry
|
||||
4. Kubernetes Deploy → Staging
|
||||
5. Integration Tests → Validation
|
||||
6. Blue-Green Deploy → Production
|
||||
```
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Service Communication
|
||||
- RESTful APIs for synchronous calls
|
||||
- RabbitMQ for asynchronous messaging
|
||||
- gRPC for high-performance internal communication
|
||||
|
||||
### Error Handling
|
||||
- Circuit breaker pattern
|
||||
- Retry with exponential backoff
|
||||
- Dead letter queues
|
||||
- Graceful degradation
|
||||
|
||||
### Logging & Monitoring
|
||||
- Structured logging (JSON)
|
||||
- Correlation IDs
|
||||
- Distributed tracing
|
||||
- Custom metrics
|
||||
|
||||
### Testing Strategy
|
||||
- Unit tests (>80% coverage)
|
||||
- Integration tests
|
||||
- End-to-end tests
|
||||
- Performance tests
|
||||
- Security tests
|
||||
402
marketing-agent/docs/deployment/README.md
Normal file
402
marketing-agent/docs/deployment/README.md
Normal file
@@ -0,0 +1,402 @@
|
||||
# Deployment Guide for Marketing Intelligence Agent System
|
||||
|
||||
This guide covers various deployment options for the Marketing Intelligence Agent System.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Docker Deployment](#docker-deployment)
|
||||
- [Kubernetes Deployment](#kubernetes-deployment)
|
||||
- [CI/CD Pipeline](#cicd-pipeline)
|
||||
- [Environment Configuration](#environment-configuration)
|
||||
- [Security Best Practices](#security-best-practices)
|
||||
- [Monitoring and Maintenance](#monitoring-and-maintenance)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### System Requirements
|
||||
|
||||
- **CPU**: Minimum 8 cores (16 cores recommended for production)
|
||||
- **Memory**: Minimum 16GB RAM (32GB recommended for production)
|
||||
- **Storage**: Minimum 100GB SSD storage
|
||||
- **Network**: Stable internet connection with low latency
|
||||
|
||||
### Software Requirements
|
||||
|
||||
- Docker 20.10+ and Docker Compose 2.0+
|
||||
- Kubernetes 1.24+ (for K8s deployment)
|
||||
- Helm 3.10+ (for K8s deployment)
|
||||
- Git
|
||||
- Node.js 18+ (for local development)
|
||||
|
||||
### External Services
|
||||
|
||||
- Telegram account with API credentials
|
||||
- Anthropic API key (for Claude AI)
|
||||
- OpenAI API key (optional, for safety checks)
|
||||
- SMTP server or email service (for notifications)
|
||||
- S3-compatible storage (for backups)
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
### Quick Start
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/yourcompany/marketing-agent.git
|
||||
cd marketing-agent
|
||||
```
|
||||
|
||||
2. Copy environment template:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
3. Edit `.env` file with your configurations:
|
||||
```bash
|
||||
# Required configurations
|
||||
ANTHROPIC_API_KEY=your_anthropic_api_key
|
||||
JWT_SECRET=your_jwt_secret
|
||||
MONGO_ROOT_PASSWORD=secure_password
|
||||
REDIS_PASSWORD=secure_password
|
||||
# ... other configurations
|
||||
```
|
||||
|
||||
4. Build and start services:
|
||||
```bash
|
||||
# Build all images
|
||||
./scripts/docker-build.sh
|
||||
|
||||
# Start services
|
||||
docker-compose up -d
|
||||
|
||||
# Wait for services to be ready
|
||||
./scripts/wait-for-services.sh
|
||||
```
|
||||
|
||||
5. Initialize admin user:
|
||||
```bash
|
||||
docker-compose exec api-gateway node scripts/init-admin.js
|
||||
```
|
||||
|
||||
### Production Deployment
|
||||
|
||||
For production deployment, use the production compose file:
|
||||
|
||||
```bash
|
||||
# Build with production optimizations
|
||||
./scripts/docker-build.sh --tag v1.0.0 --registry ghcr.io/yourcompany
|
||||
|
||||
# Deploy with production settings
|
||||
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||||
```
|
||||
|
||||
### SSL/TLS Configuration
|
||||
|
||||
1. Place SSL certificates in `infrastructure/ssl/`:
|
||||
```
|
||||
infrastructure/ssl/
|
||||
├── cert.pem
|
||||
├── key.pem
|
||||
└── dhparam.pem
|
||||
```
|
||||
|
||||
2. Update nginx configuration:
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||
# ... other SSL settings
|
||||
}
|
||||
```
|
||||
|
||||
## Kubernetes Deployment
|
||||
|
||||
### Using Helm
|
||||
|
||||
1. Add Helm dependencies:
|
||||
```bash
|
||||
cd helm/marketing-agent
|
||||
helm dependency update
|
||||
```
|
||||
|
||||
2. Create namespace:
|
||||
```bash
|
||||
kubectl create namespace marketing-agent
|
||||
```
|
||||
|
||||
3. Create secrets:
|
||||
```bash
|
||||
kubectl create secret generic marketing-agent-secrets \
|
||||
--from-literal=anthropic-api-key=$ANTHROPIC_API_KEY \
|
||||
--from-literal=jwt-secret=$JWT_SECRET \
|
||||
-n marketing-agent
|
||||
```
|
||||
|
||||
4. Install the chart:
|
||||
```bash
|
||||
helm install marketing-agent ./helm/marketing-agent \
|
||||
--namespace marketing-agent \
|
||||
--values ./helm/marketing-agent/values.yaml \
|
||||
--values ./helm/marketing-agent/values.prod.yaml
|
||||
```
|
||||
|
||||
### Manual Kubernetes Deployment
|
||||
|
||||
If not using Helm, apply the manifests directly:
|
||||
|
||||
```bash
|
||||
kubectl apply -f infrastructure/k8s/namespace.yaml
|
||||
kubectl apply -f infrastructure/k8s/configmap.yaml
|
||||
kubectl apply -f infrastructure/k8s/secrets.yaml
|
||||
kubectl apply -f infrastructure/k8s/deployments/
|
||||
kubectl apply -f infrastructure/k8s/services/
|
||||
kubectl apply -f infrastructure/k8s/ingress.yaml
|
||||
```
|
||||
|
||||
### Scaling
|
||||
|
||||
Horizontal Pod Autoscaling is configured for key services:
|
||||
|
||||
```bash
|
||||
# Check HPA status
|
||||
kubectl get hpa -n marketing-agent
|
||||
|
||||
# Manual scaling
|
||||
kubectl scale deployment marketing-agent-api-gateway --replicas=5 -n marketing-agent
|
||||
```
|
||||
|
||||
## CI/CD Pipeline
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
The project includes comprehensive CI/CD pipelines:
|
||||
|
||||
#### Continuous Integration (.github/workflows/ci.yml)
|
||||
- Code linting and formatting
|
||||
- Security scanning
|
||||
- Unit and integration tests
|
||||
- Docker image building
|
||||
- E2E testing
|
||||
- Performance testing
|
||||
|
||||
#### Continuous Deployment (.github/workflows/cd.yml)
|
||||
- Automatic deployment to staging on main branch
|
||||
- Production deployment on version tags
|
||||
- Database migrations
|
||||
- Blue-green deployment
|
||||
- Automated rollback on failure
|
||||
|
||||
#### Security Scanning (.github/workflows/security.yml)
|
||||
- Container vulnerability scanning
|
||||
- SAST (Static Application Security Testing)
|
||||
- Secret scanning
|
||||
- Dependency auditing
|
||||
- Infrastructure security checks
|
||||
|
||||
### Setting up CI/CD
|
||||
|
||||
1. Configure GitHub secrets:
|
||||
```
|
||||
AWS_ACCESS_KEY_ID
|
||||
AWS_SECRET_ACCESS_KEY
|
||||
ANTHROPIC_API_KEY
|
||||
SLACK_WEBHOOK
|
||||
DOCKER_REGISTRY_USERNAME
|
||||
DOCKER_REGISTRY_PASSWORD
|
||||
```
|
||||
|
||||
2. Configure deployment environments in GitHub:
|
||||
- staging
|
||||
- production
|
||||
|
||||
3. Enable branch protection rules for main branch
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create environment-specific files:
|
||||
- `.env.development`
|
||||
- `.env.staging`
|
||||
- `.env.production`
|
||||
|
||||
Key configurations:
|
||||
|
||||
```bash
|
||||
# Application
|
||||
NODE_ENV=production
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Security
|
||||
JWT_SECRET=<generate-with-openssl-rand>
|
||||
ENCRYPTION_KEY=<32-byte-key>
|
||||
|
||||
# Database
|
||||
MONGODB_URI=mongodb://user:pass@mongodb:27017/marketing_agent
|
||||
REDIS_PASSWORD=<secure-password>
|
||||
|
||||
# External Services
|
||||
ANTHROPIC_API_KEY=<your-key>
|
||||
TELEGRAM_SYSTEM_URL=<your-telegram-system-url>
|
||||
|
||||
# Monitoring
|
||||
GRAFANA_ADMIN_PASSWORD=<secure-password>
|
||||
ELASTIC_PASSWORD=<secure-password>
|
||||
```
|
||||
|
||||
### Configuration Management
|
||||
|
||||
Use ConfigMaps for non-sensitive configurations:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: marketing-agent-config
|
||||
data:
|
||||
NODE_ENV: "production"
|
||||
LOG_LEVEL: "info"
|
||||
RATE_LIMIT_WINDOW: "60000"
|
||||
RATE_LIMIT_MAX: "100"
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### 1. Secret Management
|
||||
|
||||
- Use Kubernetes secrets or external secret managers (Vault, AWS Secrets Manager)
|
||||
- Rotate secrets regularly
|
||||
- Never commit secrets to version control
|
||||
|
||||
### 2. Network Security
|
||||
|
||||
- Enable network policies to restrict traffic
|
||||
- Use TLS for all communications
|
||||
- Implement proper CORS policies
|
||||
|
||||
### 3. Container Security
|
||||
|
||||
- Run containers as non-root user
|
||||
- Use minimal base images (Alpine)
|
||||
- Scan images for vulnerabilities
|
||||
- Keep dependencies updated
|
||||
|
||||
### 4. Access Control
|
||||
|
||||
- Implement RBAC in Kubernetes
|
||||
- Use service accounts with minimal permissions
|
||||
- Enable audit logging
|
||||
|
||||
### 5. Data Protection
|
||||
|
||||
- Encrypt data at rest
|
||||
- Use TLS for data in transit
|
||||
- Implement proper backup encryption
|
||||
- Follow GDPR compliance guidelines
|
||||
|
||||
## Monitoring and Maintenance
|
||||
|
||||
### Health Checks
|
||||
|
||||
All services expose health endpoints:
|
||||
- `/health` - Basic health check
|
||||
- `/health/live` - Liveness probe
|
||||
- `/health/ready` - Readiness probe
|
||||
|
||||
### Metrics and Monitoring
|
||||
|
||||
1. Access Grafana dashboards:
|
||||
```
|
||||
http://localhost:3032 (Docker)
|
||||
https://grafana.your-domain.com (K8s)
|
||||
```
|
||||
|
||||
2. Key metrics to monitor:
|
||||
- API response times
|
||||
- Error rates
|
||||
- Message delivery success rate
|
||||
- System resource usage
|
||||
- Database performance
|
||||
|
||||
### Logging
|
||||
|
||||
Centralized logging with Elasticsearch:
|
||||
|
||||
```bash
|
||||
# View logs
|
||||
docker-compose logs -f api-gateway
|
||||
|
||||
# Search logs in Elasticsearch
|
||||
curl -X GET "localhost:9200/logs-*/_search?q=error"
|
||||
```
|
||||
|
||||
### Backup and Recovery
|
||||
|
||||
Automated backups are configured to run daily:
|
||||
|
||||
```bash
|
||||
# Manual backup
|
||||
./scripts/backup.sh
|
||||
|
||||
# Restore from backup
|
||||
./scripts/restore.sh backup-20240115-020000.tar.gz
|
||||
```
|
||||
|
||||
### Maintenance Tasks
|
||||
|
||||
1. **Database maintenance**:
|
||||
```bash
|
||||
# MongoDB optimization
|
||||
docker-compose exec mongodb mongosh --eval "db.adminCommand({compact: 'campaigns'})"
|
||||
|
||||
# Redis memory optimization
|
||||
docker-compose exec redis redis-cli --raw MEMORY DOCTOR
|
||||
```
|
||||
|
||||
2. **Log rotation**:
|
||||
```bash
|
||||
# Configured in Docker with max-size and max-file
|
||||
# For K8s, use log shipping solutions
|
||||
```
|
||||
|
||||
3. **Update dependencies**:
|
||||
```bash
|
||||
# Check for updates
|
||||
npm audit
|
||||
|
||||
# Update dependencies
|
||||
npm update
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
Common issues and solutions:
|
||||
|
||||
1. **Service not starting**:
|
||||
- Check logs: `docker-compose logs <service>`
|
||||
- Verify environment variables
|
||||
- Check resource availability
|
||||
|
||||
2. **Database connection issues**:
|
||||
- Verify connection strings
|
||||
- Check network connectivity
|
||||
- Ensure database is initialized
|
||||
|
||||
3. **Performance issues**:
|
||||
- Check resource usage
|
||||
- Review slow query logs
|
||||
- Enable performance profiling
|
||||
|
||||
4. **Deployment failures**:
|
||||
- Check CI/CD logs
|
||||
- Verify credentials and permissions
|
||||
- Review deployment prerequisites
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Review [API Documentation](../api/README.md)
|
||||
- Configure [Monitoring Dashboards](../monitoring/README.md)
|
||||
- Set up [Backup Strategy](../backup/README.md)
|
||||
- Implement [Disaster Recovery Plan](../disaster-recovery/README.md)
|
||||
BIN
marketing-agent/error_screenshot.png
Normal file
BIN
marketing-agent/error_screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
marketing-agent/final_screenshot.png
Normal file
BIN
marketing-agent/final_screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
52
marketing-agent/fix-permissions.sh
Executable file
52
marketing-agent/fix-permissions.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Fix permissions for all services
|
||||
# This script updates Dockerfiles to ensure proper permissions
|
||||
|
||||
echo "Fixing permissions in Dockerfiles..."
|
||||
|
||||
# Function to add logs directory creation to Dockerfile
|
||||
fix_dockerfile() {
|
||||
local service=$1
|
||||
local dockerfile="services/$service/Dockerfile"
|
||||
|
||||
if [ -f "$dockerfile" ]; then
|
||||
echo "Fixing $dockerfile..."
|
||||
|
||||
# Check if logs directory creation already exists
|
||||
if ! grep -q "RUN mkdir -p logs" "$dockerfile"; then
|
||||
# Add logs directory creation before the last RUN command
|
||||
sed -i '' '/RUN adduser -S nodejs -u 1001/a\
|
||||
\
|
||||
# Create logs directory with proper permissions\
|
||||
RUN mkdir -p logs && chown -R nodejs:nodejs logs' "$dockerfile"
|
||||
fi
|
||||
|
||||
# Check if USER nodejs is set
|
||||
if ! grep -q "USER nodejs" "$dockerfile"; then
|
||||
# Add USER nodejs at the end before CMD
|
||||
echo "" >> "$dockerfile"
|
||||
echo "# Switch to non-root user" >> "$dockerfile"
|
||||
echo "USER nodejs" >> "$dockerfile"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Fix all service Dockerfiles
|
||||
services=(
|
||||
"api-gateway"
|
||||
"orchestrator"
|
||||
"claude-agent"
|
||||
"gramjs-adapter"
|
||||
"safety-guard"
|
||||
"analytics"
|
||||
"compliance-guard"
|
||||
"ab-testing"
|
||||
)
|
||||
|
||||
for service in "${services[@]}"; do
|
||||
fix_dockerfile "$service"
|
||||
done
|
||||
|
||||
echo "Permissions fixed! Now rebuild the services:"
|
||||
echo "docker-compose build"
|
||||
29
marketing-agent/frontend/.gitignore
vendored
Normal file
29
marketing-agent/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
47
marketing-agent/frontend/Dockerfile
Normal file
47
marketing-agent/frontend/Dockerfile
Normal file
@@ -0,0 +1,47 @@
|
||||
# Build stage
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application with production environment
|
||||
ARG VITE_API_URL=/api
|
||||
ARG VITE_ENVIRONMENT=production
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
|
||||
# Install dumb-init for proper signal handling
|
||||
RUN apk add --no-cache dumb-init
|
||||
|
||||
# Copy built files from builder
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Copy nginx configuration
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Create cache directory for nginx
|
||||
RUN mkdir -p /var/cache/nginx && \
|
||||
chown -R nginx:nginx /var/cache/nginx
|
||||
|
||||
# Expose port
|
||||
EXPOSE 80
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1
|
||||
|
||||
# Use dumb-init to handle signals
|
||||
ENTRYPOINT ["dumb-init", "--"]
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
203
marketing-agent/frontend/MOBILE_RESPONSIVE.md
Normal file
203
marketing-agent/frontend/MOBILE_RESPONSIVE.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# Mobile Responsive Support
|
||||
|
||||
This document describes the mobile responsive implementation for the Marketing Agent System frontend.
|
||||
|
||||
## Overview
|
||||
|
||||
The frontend has been enhanced with comprehensive mobile responsive support, providing an optimized experience across all device sizes.
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Responsive Layout System
|
||||
|
||||
- **Automatic Layout Switching**: The application automatically detects device type and switches between desktop and mobile layouts
|
||||
- **Breakpoints**:
|
||||
- Mobile: < 768px
|
||||
- Tablet: 768px - 1024px
|
||||
- Desktop: > 1024px
|
||||
|
||||
### 2. Mobile-Specific Components
|
||||
|
||||
#### Mobile Layout (`LayoutMobile.vue`)
|
||||
- Hamburger menu for navigation
|
||||
- Bottom navigation bar for quick access to main features
|
||||
- Optimized header with essential actions
|
||||
- Slide-out sidebar for full menu access
|
||||
|
||||
#### Mobile Dashboard (`DashboardMobile.vue`)
|
||||
- Compact stat cards in 2-column grid
|
||||
- Optimized charts with mobile-friendly options
|
||||
- Quick action floating button
|
||||
- Touch-friendly interface elements
|
||||
|
||||
#### Mobile Campaign List (`CampaignListMobile.vue`)
|
||||
- Card-based layout for better readability
|
||||
- Swipe actions for quick operations
|
||||
- Load more pagination instead of traditional pagination
|
||||
- Inline stats and progress indicators
|
||||
|
||||
#### Mobile Analytics (`AnalyticsMobile.vue`)
|
||||
- Scrollable metric cards
|
||||
- Responsive charts with touch interactions
|
||||
- Collapsible sections for better organization
|
||||
- Export options via floating action button
|
||||
|
||||
### 3. Responsive Utilities
|
||||
|
||||
#### `useResponsive` Composable
|
||||
```javascript
|
||||
import { useResponsive } from '@/composables/useResponsive'
|
||||
|
||||
const { isMobile, isTablet, isDesktop, screenWidth, screenHeight } = useResponsive()
|
||||
```
|
||||
|
||||
### 4. Mobile-Optimized Styles
|
||||
|
||||
- Touch-friendly button sizes (minimum 44px height)
|
||||
- Larger input fields to prevent zoom on iOS
|
||||
- Optimized spacing for mobile screens
|
||||
- Smooth scrolling with momentum
|
||||
- Bottom safe area padding for iOS devices
|
||||
|
||||
### 5. Performance Optimizations
|
||||
|
||||
- Lazy loading for mobile components
|
||||
- Reduced data fetching on mobile
|
||||
- Optimized images and assets
|
||||
- Simplified animations for better performance
|
||||
|
||||
## Implementation Guide
|
||||
|
||||
### Using Responsive Components
|
||||
|
||||
1. **In Views**: Components automatically detect mobile and render appropriate version
|
||||
```vue
|
||||
<template>
|
||||
<DashboardMobile v-if="isMobile" />
|
||||
<div v-else>
|
||||
<!-- Desktop content -->
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
2. **Responsive Classes**: Use Tailwind responsive prefixes
|
||||
```html
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
|
||||
<!-- Responsive grid -->
|
||||
</div>
|
||||
```
|
||||
|
||||
3. **Mobile-First Approach**: Design for mobile first, then enhance for larger screens
|
||||
|
||||
### Touch Interactions
|
||||
|
||||
- Swipe gestures for navigation and actions
|
||||
- Pull-to-refresh on scrollable lists
|
||||
- Long press for context menus
|
||||
- Pinch-to-zoom disabled on UI elements
|
||||
|
||||
### Navigation Patterns
|
||||
|
||||
1. **Bottom Navigation**: Primary navigation for most-used features
|
||||
2. **Hamburger Menu**: Full navigation access via slide-out menu
|
||||
3. **Floating Action Buttons**: Quick access to primary actions
|
||||
4. **Breadcrumbs**: Simplified on mobile, showing only current location
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Content Prioritization
|
||||
- Show most important information first
|
||||
- Use progressive disclosure for details
|
||||
- Minimize cognitive load with clear hierarchy
|
||||
|
||||
### 2. Touch Targets
|
||||
- Minimum 44x44px for all interactive elements
|
||||
- Adequate spacing between clickable items
|
||||
- Clear visual feedback for touch interactions
|
||||
|
||||
### 3. Forms
|
||||
- Use appropriate input types (email, tel, number)
|
||||
- Single column layout for forms
|
||||
- Clear labels and error messages
|
||||
- Auto-focus management for better UX
|
||||
|
||||
### 4. Performance
|
||||
- Minimize JavaScript execution on scroll
|
||||
- Use CSS transforms for animations
|
||||
- Lazy load images and components
|
||||
- Reduce API calls with smart caching
|
||||
|
||||
### 5. Accessibility
|
||||
- Proper ARIA labels for screen readers
|
||||
- Sufficient color contrast
|
||||
- Focus management for keyboard navigation
|
||||
- Touch-friendly alternatives for hover states
|
||||
|
||||
## Testing
|
||||
|
||||
### Device Testing
|
||||
Test on real devices when possible:
|
||||
- iOS Safari (iPhone/iPad)
|
||||
- Android Chrome
|
||||
- Android Firefox
|
||||
- Various screen sizes and orientations
|
||||
|
||||
### Browser DevTools
|
||||
- Use responsive mode in Chrome/Firefox DevTools
|
||||
- Test touch events and gestures
|
||||
- Verify performance on throttled connections
|
||||
|
||||
### Automated Testing
|
||||
- Viewport-specific tests
|
||||
- Touch event simulation
|
||||
- Performance budgets for mobile
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Complex Tables**: Simplified on mobile with essential columns only
|
||||
2. **Advanced Filters**: Moved to dedicated modal on mobile
|
||||
3. **Drag & Drop**: Touch-friendly alternatives provided
|
||||
4. **Hover States**: Replaced with tap interactions
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Progressive Web App (PWA)**
|
||||
- Offline support
|
||||
- Install to home screen
|
||||
- Push notifications
|
||||
|
||||
2. **Advanced Gestures**
|
||||
- Swipe between views
|
||||
- Pull-to-refresh on all lists
|
||||
- Gesture-based shortcuts
|
||||
|
||||
3. **Adaptive Loading**
|
||||
- Lower quality images on slow connections
|
||||
- Reduced data mode
|
||||
- Progressive enhancement based on device capabilities
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **iOS Zoom on Input Focus**
|
||||
- Solution: Set font-size to 16px on inputs
|
||||
|
||||
2. **Bottom Bar Overlap on iOS**
|
||||
- Solution: Use `env(safe-area-inset-bottom)`
|
||||
|
||||
3. **Horizontal Scroll**
|
||||
- Solution: Check for elements exceeding viewport width
|
||||
|
||||
4. **Performance Issues**
|
||||
- Solution: Profile with Chrome DevTools, reduce re-renders
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable mobile debug mode in development:
|
||||
```javascript
|
||||
// In .env.development
|
||||
VITE_MOBILE_DEBUG=true
|
||||
```
|
||||
|
||||
This will show device info and performance metrics on mobile devices.
|
||||
241
marketing-agent/frontend/PERFORMANCE_OPTIMIZATION.md
Normal file
241
marketing-agent/frontend/PERFORMANCE_OPTIMIZATION.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# Frontend Performance Optimization Guide
|
||||
|
||||
This guide documents all performance optimizations implemented in the Telegram Marketing Agent frontend.
|
||||
|
||||
## Overview
|
||||
|
||||
The frontend has been optimized for performance across multiple dimensions:
|
||||
- Initial load time
|
||||
- Runtime performance
|
||||
- Memory usage
|
||||
- Network efficiency
|
||||
- User experience
|
||||
|
||||
## Build Optimizations
|
||||
|
||||
### 1. Code Splitting
|
||||
- Automatic vendor chunks for better caching
|
||||
- Manual chunks for large libraries (Element Plus, Chart.js)
|
||||
- Route-based code splitting with lazy loading
|
||||
|
||||
### 2. Compression
|
||||
- Gzip compression for all assets > 10KB
|
||||
- Brotli compression for better compression ratios
|
||||
- Image optimization with quality settings
|
||||
|
||||
### 3. Asset Optimization
|
||||
- Proper asset naming for cache busting
|
||||
- Inline small assets (< 4KB)
|
||||
- Organized asset directories
|
||||
|
||||
## Runtime Optimizations
|
||||
|
||||
### 1. Lazy Loading
|
||||
- **Images**: Intersection Observer-based lazy loading
|
||||
- **Components**: Dynamic imports with loading/error states
|
||||
- **Routes**: Lazy-loaded route components
|
||||
|
||||
```vue
|
||||
<!-- Image lazy loading -->
|
||||
<img v-lazy="imageSrc" :alt="imageAlt">
|
||||
|
||||
<!-- Background lazy loading -->
|
||||
<div v-lazy-bg="backgroundUrl"></div>
|
||||
|
||||
<!-- Progressive image loading -->
|
||||
<img v-progressive="{ lowQuality: thumbUrl, highQuality: fullUrl }">
|
||||
```
|
||||
|
||||
### 2. Virtual Scrolling
|
||||
For large lists, use the VirtualList component:
|
||||
|
||||
```vue
|
||||
<VirtualList
|
||||
:items="items"
|
||||
:item-height="50"
|
||||
v-slot="{ item }"
|
||||
>
|
||||
<div>{{ item.name }}</div>
|
||||
</VirtualList>
|
||||
```
|
||||
|
||||
### 3. Web Workers
|
||||
Heavy computations are offloaded to web workers:
|
||||
|
||||
```javascript
|
||||
import { useComputationWorker } from '@/composables/useWebWorker'
|
||||
|
||||
const { sort, filter, aggregate, loading, result } = useComputationWorker()
|
||||
|
||||
// Sort large dataset
|
||||
await sort(largeArray)
|
||||
|
||||
// Filter data
|
||||
await filter(items, 'status', 'active')
|
||||
```
|
||||
|
||||
### 4. Debouncing & Throttling
|
||||
```javascript
|
||||
import { debounce, throttle } from '@/utils/performance'
|
||||
|
||||
// Debounce search input
|
||||
const search = debounce((query) => {
|
||||
// Search logic
|
||||
}, 300)
|
||||
|
||||
// Throttle scroll handler
|
||||
const handleScroll = throttle(() => {
|
||||
// Scroll logic
|
||||
}, 100)
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
### 1. Persisted State
|
||||
Store state is automatically persisted with debouncing:
|
||||
|
||||
```javascript
|
||||
// In store configuration
|
||||
export const useUserStore = defineStore('user', {
|
||||
persist: {
|
||||
paths: ['profile', 'preferences'],
|
||||
debounceTime: 1000
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Memory Management
|
||||
- WeakMap for object references
|
||||
- Automatic cleanup on component unmount
|
||||
- Memory leak detection in development
|
||||
|
||||
## Network Optimizations
|
||||
|
||||
### 1. Service Worker
|
||||
- Offline support with cache strategies
|
||||
- Background sync for failed requests
|
||||
- Push notifications support
|
||||
|
||||
### 2. API Caching
|
||||
- Cache-first for static data
|
||||
- Network-first with cache fallback for dynamic data
|
||||
- Stale-while-revalidate for frequently updated data
|
||||
|
||||
### 3. Request Optimization
|
||||
- Request batching for multiple API calls
|
||||
- Request deduplication
|
||||
- Automatic retry with exponential backoff
|
||||
|
||||
## Monitoring & Analytics
|
||||
|
||||
### 1. Performance Metrics
|
||||
The app automatically tracks:
|
||||
- First Contentful Paint (FCP)
|
||||
- Largest Contentful Paint (LCP)
|
||||
- First Input Delay (FID)
|
||||
- Cumulative Layout Shift (CLS)
|
||||
- Time to Interactive (TTI)
|
||||
|
||||
### 2. Error Tracking
|
||||
- Global error handler
|
||||
- Source maps for production debugging
|
||||
- User context in error reports
|
||||
|
||||
### 3. User Analytics
|
||||
- Page view tracking
|
||||
- User interaction events
|
||||
- Performance impact analysis
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Component Development
|
||||
- Use `shallowRef` for large objects
|
||||
- Implement `v-memo` for expensive renders
|
||||
- Use `computed` instead of methods for derived state
|
||||
|
||||
### 2. Event Handling
|
||||
- Use passive event listeners
|
||||
- Delegate events when possible
|
||||
- Clean up listeners on unmount
|
||||
|
||||
### 3. Asset Loading
|
||||
- Preload critical resources
|
||||
- Use resource hints (prefetch, preconnect)
|
||||
- Implement responsive images
|
||||
|
||||
### 4. Bundle Size
|
||||
- Tree-shake unused code
|
||||
- Use dynamic imports for optional features
|
||||
- Monitor bundle size with visualizer
|
||||
|
||||
## Development Tools
|
||||
|
||||
### 1. Performance Profiling
|
||||
```bash
|
||||
# Generate bundle analysis
|
||||
npm run build -- --report
|
||||
|
||||
# Profile runtime performance
|
||||
window.__PERFORMANCE__.getMetrics()
|
||||
```
|
||||
|
||||
### 2. Lighthouse CI
|
||||
Run Lighthouse in CI to track performance over time:
|
||||
```bash
|
||||
npm run lighthouse
|
||||
```
|
||||
|
||||
### 3. Memory Profiling
|
||||
Use Chrome DevTools Memory Profiler to identify leaks.
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### vite.config.optimized.js
|
||||
Contains all build optimizations including:
|
||||
- Code splitting configuration
|
||||
- Compression plugins
|
||||
- Asset optimization
|
||||
- Performance hints
|
||||
|
||||
### Performance Budget
|
||||
Target metrics:
|
||||
- FCP: < 1.8s
|
||||
- LCP: < 2.5s
|
||||
- TTI: < 3.8s
|
||||
- Bundle size: < 500KB (initial)
|
||||
|
||||
## Checklist
|
||||
|
||||
Before deploying, ensure:
|
||||
- [ ] Images are optimized and lazy-loaded
|
||||
- [ ] Large lists use virtual scrolling
|
||||
- [ ] Heavy computations use web workers
|
||||
- [ ] API calls are cached appropriately
|
||||
- [ ] Service worker is registered (production)
|
||||
- [ ] Performance metrics are within budget
|
||||
- [ ] No memory leaks detected
|
||||
- [ ] Bundle size is optimized
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### High Memory Usage
|
||||
1. Check for detached DOM nodes
|
||||
2. Review event listener cleanup
|
||||
3. Verify store subscription cleanup
|
||||
|
||||
### Slow Initial Load
|
||||
1. Review bundle splitting
|
||||
2. Check for blocking resources
|
||||
3. Verify compression is working
|
||||
|
||||
### Poor Runtime Performance
|
||||
1. Profile with Chrome DevTools
|
||||
2. Check for unnecessary re-renders
|
||||
3. Review computed property usage
|
||||
|
||||
## Future Optimizations
|
||||
|
||||
1. **HTTP/3 Support**: When available
|
||||
2. **Module Federation**: For micro-frontends
|
||||
3. **Edge Computing**: For global performance
|
||||
4. **AI-Powered Prefetching**: Predictive resource loading
|
||||
217
marketing-agent/frontend/README.md
Normal file
217
marketing-agent/frontend/README.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# Telegram Marketing Agent - Frontend
|
||||
|
||||
Modern Vue 3 management interface for the Telegram Marketing Agent System.
|
||||
|
||||
## Features
|
||||
|
||||
- 🎯 **Campaign Management**: Create, manage, and monitor marketing campaigns
|
||||
- 📊 **Real-time Analytics**: Track performance metrics and engagement rates
|
||||
- 🧠 **A/B Testing**: Built-in experimentation framework
|
||||
- 🤖 **AI Integration**: Claude-powered strategy generation and optimization
|
||||
- 🔐 **Compliance Tools**: GDPR/CCPA compliance management
|
||||
- 🌍 **Multi-language Support**: English and Chinese interfaces
|
||||
- 🎨 **Modern UI**: Built with Element Plus and Tailwind CSS
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: Vue 3 with Composition API
|
||||
- **State Management**: Pinia
|
||||
- **Routing**: Vue Router 4
|
||||
- **UI Library**: Element Plus
|
||||
- **Styling**: Tailwind CSS
|
||||
- **Build Tool**: Vite
|
||||
- **Charts**: Chart.js with vue-chartjs
|
||||
- **HTTP Client**: Axios
|
||||
- **Internationalization**: Vue I18n
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+ and npm 8+
|
||||
- Backend services running (see main README)
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start development server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The application will be available at http://localhost:3008
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
The frontend proxies API requests to the backend. Configure the proxy in `vite.config.js`:
|
||||
|
||||
```javascript
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000', // API Gateway URL
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/ # API client modules
|
||||
│ ├── index.js # Axios configuration
|
||||
│ └── modules/ # API endpoints by domain
|
||||
├── assets/ # Static assets
|
||||
├── components/ # Reusable components
|
||||
├── locales/ # i18n translations
|
||||
├── router/ # Vue Router configuration
|
||||
├── stores/ # Pinia stores
|
||||
├── utils/ # Utility functions
|
||||
├── views/ # Page components
|
||||
├── App.vue # Root component
|
||||
├── main.js # Application entry
|
||||
└── style.css # Global styles
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### Campaign Management
|
||||
|
||||
- Create campaigns with AI-powered strategy generation
|
||||
- Multi-step message sequences with delays
|
||||
- Target audience selection by groups and tags
|
||||
- Real-time campaign status monitoring
|
||||
- Campaign lifecycle management (start, pause, resume, cancel)
|
||||
|
||||
### Analytics Dashboard
|
||||
|
||||
- Key performance metrics with trend analysis
|
||||
- Time-series charts for message and engagement data
|
||||
- Campaign performance comparison
|
||||
- Real-time activity feed
|
||||
- Export reports in multiple formats
|
||||
|
||||
### A/B Testing
|
||||
|
||||
- Create experiments with multiple variants
|
||||
- Statistical significance testing
|
||||
- Real-time result monitoring
|
||||
- Winner selection and rollout
|
||||
|
||||
### Compliance Management
|
||||
|
||||
- User consent tracking
|
||||
- Data export/deletion requests
|
||||
- Audit log viewing
|
||||
- GDPR/CCPA compliance status
|
||||
|
||||
## Development
|
||||
|
||||
### Available Scripts
|
||||
|
||||
```bash
|
||||
# Development server
|
||||
npm run dev
|
||||
|
||||
# Production build
|
||||
npm run build
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
|
||||
# Lint and fix
|
||||
npm run lint
|
||||
|
||||
# Format code
|
||||
npm run format
|
||||
```
|
||||
|
||||
### Code Style
|
||||
|
||||
- Use Composition API with `<script setup>`
|
||||
- Prefer TypeScript-like prop definitions
|
||||
- Use Tailwind for utility classes
|
||||
- Use Element Plus components for consistency
|
||||
|
||||
### State Management
|
||||
|
||||
The application uses Pinia for state management:
|
||||
|
||||
```javascript
|
||||
// stores/auth.js
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref(null)
|
||||
const token = ref('')
|
||||
|
||||
// Store logic...
|
||||
})
|
||||
```
|
||||
|
||||
### API Integration
|
||||
|
||||
All API calls go through the centralized API client:
|
||||
|
||||
```javascript
|
||||
import api from '@/api'
|
||||
|
||||
// Example usage
|
||||
const campaigns = await api.campaigns.getList({
|
||||
page: 1,
|
||||
pageSize: 20
|
||||
})
|
||||
```
|
||||
|
||||
## Building for Production
|
||||
|
||||
```bash
|
||||
# Build the application
|
||||
npm run build
|
||||
|
||||
# Files will be in dist/
|
||||
# Serve with any static file server
|
||||
```
|
||||
|
||||
### Nginx Configuration
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
root /var/www/marketing-agent;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://api-gateway:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- All API requests require authentication
|
||||
- JWT tokens stored in localStorage
|
||||
- Automatic token refresh on 401 responses
|
||||
- CORS configured for production domains
|
||||
- Input validation on all forms
|
||||
- XSS protection via Vue's template compilation
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Follow the existing code style
|
||||
2. Write meaningful commit messages
|
||||
3. Add appropriate error handling
|
||||
4. Update translations for new features
|
||||
5. Test across different screen sizes
|
||||
|
||||
## License
|
||||
|
||||
See the main project LICENSE file.
|
||||
13
marketing-agent/frontend/index.html
Normal file
13
marketing-agent/frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Telegram Marketing Agent</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
66
marketing-agent/frontend/nginx.conf
Normal file
66
marketing-agent/frontend/nginx.conf
Normal file
@@ -0,0 +1,66 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Enable gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript application/json;
|
||||
|
||||
# API proxy
|
||||
location /api/ {
|
||||
proxy_pass http://api-gateway:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
# Timeouts
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# WebSocket support
|
||||
location /socket.io/ {
|
||||
proxy_pass http://api-gateway:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Vue Router support - serve index.html for all routes
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
|
||||
# Cache static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
50
marketing-agent/frontend/package.json
Normal file
50
marketing-agent/frontend/package.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "marketing-agent-frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "Frontend management interface for Telegram Marketing Agent System",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:analyze": "vite build --mode analyze",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx --fix",
|
||||
"format": "prettier --write src/",
|
||||
"lighthouse": "lighthouse http://localhost:3008 --output html --output-path ./lighthouse-report.html",
|
||||
"lighthouse:ci": "lighthouse http://localhost:3008 --output json --output-path ./lighthouse-report.json --chrome-flags='--headless'",
|
||||
"test:performance": "npm run build && npm run preview & sleep 5 && npm run lighthouse:ci"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.6.5",
|
||||
"chart.js": "^4.4.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"element-plus": "^2.4.4",
|
||||
"lodash-es": "^4.17.21",
|
||||
"pinia": "^2.1.7",
|
||||
"socket.io-client": "^4.6.0",
|
||||
"vue": "^3.4.15",
|
||||
"vue-chartjs": "^5.3.0",
|
||||
"vue-i18n": "^9.9.0",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.20.1",
|
||||
"lighthouse": "^11.4.0",
|
||||
"postcss": "^8.4.33",
|
||||
"prettier": "^3.2.4",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.70.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"unplugin-auto-import": "^0.17.3",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.0.11",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-imagemin": "^0.6.1",
|
||||
"workbox-webpack-plugin": "^7.0.0"
|
||||
}
|
||||
}
|
||||
6
marketing-agent/frontend/postcss.config.js
Normal file
6
marketing-agent/frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
51
marketing-agent/frontend/public/clear-and-test.html
Normal file
51
marketing-agent/frontend/public/clear-and-test.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Clear and Test</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Clear Storage and Test Login</h1>
|
||||
|
||||
<button onclick="clearStorage()">Clear All Storage</button>
|
||||
<button onclick="testAuth()">Test Current Auth</button>
|
||||
<button onclick="goToLogin()">Go to Login Page</button>
|
||||
<button onclick="goToHome()">Go to Home (Protected)</button>
|
||||
|
||||
<div id="result" style="margin-top: 20px; padding: 10px; border: 1px solid #ddd;"></div>
|
||||
|
||||
<script>
|
||||
function clearStorage() {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
document.getElementById('result').innerHTML = 'All storage cleared!';
|
||||
}
|
||||
|
||||
function testAuth() {
|
||||
const token = localStorage.getItem('token');
|
||||
const result = document.getElementById('result');
|
||||
|
||||
if (token) {
|
||||
result.innerHTML = `
|
||||
<p style="color: green;">Authenticated!</p>
|
||||
<p>Token: ${token.substring(0, 50)}...</p>
|
||||
`;
|
||||
} else {
|
||||
result.innerHTML = '<p style="color: red;">Not authenticated</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function goToLogin() {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
function goToHome() {
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
// Show current status on load
|
||||
window.onload = testAuth;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
97
marketing-agent/frontend/public/login-test.html
Normal file
97
marketing-agent/frontend/public/login-test.html
Normal file
@@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login Test</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; padding: 20px; }
|
||||
input { margin: 5px 0; padding: 5px; width: 200px; }
|
||||
button { margin: 10px 0; padding: 10px 20px; background: #4CAF50; color: white; border: none; cursor: pointer; }
|
||||
button:hover { background: #45a049; }
|
||||
.result { margin-top: 20px; padding: 10px; border: 1px solid #ddd; }
|
||||
.error { color: red; }
|
||||
.success { color: green; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Login Test</h1>
|
||||
|
||||
<div>
|
||||
<input type="text" id="username" value="admin" placeholder="Username"><br>
|
||||
<input type="password" id="password" value="admin123456" placeholder="Password"><br>
|
||||
<button onclick="login()">Login</button>
|
||||
</div>
|
||||
|
||||
<div id="result" class="result"></div>
|
||||
|
||||
<script>
|
||||
async function login() {
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const resultDiv = document.getElementById('result');
|
||||
|
||||
try {
|
||||
// Login
|
||||
const response = await axios.post('/api/v1/auth/login', {
|
||||
username: username,
|
||||
password: password
|
||||
});
|
||||
|
||||
console.log('Login response:', response.data);
|
||||
|
||||
if (response.data.success) {
|
||||
const token = response.data.data.accessToken;
|
||||
const user = response.data.data.user;
|
||||
|
||||
resultDiv.innerHTML = `
|
||||
<div class="success">
|
||||
<h3>Login Successful!</h3>
|
||||
<p>User: ${user.username} (${user.role})</p>
|
||||
<p>Token stored in localStorage</p>
|
||||
<button onclick="testDashboard()">Test Dashboard Access</button>
|
||||
<button onclick="goToApp()">Go to Application</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Store token
|
||||
localStorage.setItem('token', token);
|
||||
localStorage.setItem('refreshToken', response.data.data.refreshToken);
|
||||
} else {
|
||||
resultDiv.innerHTML = `<div class="error">Login failed: ${response.data.error}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
resultDiv.innerHTML = `<div class="error">Error: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function testDashboard() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
alert('No token found!');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/v1/analytics/dashboard', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Dashboard data:', response.data);
|
||||
alert('Dashboard access successful! Check console for data.');
|
||||
} catch (error) {
|
||||
console.error('Dashboard error:', error);
|
||||
alert('Dashboard error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function goToApp() {
|
||||
window.location.href = '/';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
246
marketing-agent/frontend/public/service-worker.js
Normal file
246
marketing-agent/frontend/public/service-worker.js
Normal file
@@ -0,0 +1,246 @@
|
||||
// Service Worker for offline support and performance optimization
|
||||
|
||||
const CACHE_NAME = 'marketing-agent-v1'
|
||||
const STATIC_CACHE_NAME = 'marketing-agent-static-v1'
|
||||
const DYNAMIC_CACHE_NAME = 'marketing-agent-dynamic-v1'
|
||||
const API_CACHE_NAME = 'marketing-agent-api-v1'
|
||||
|
||||
// Files to cache immediately
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/manifest.json',
|
||||
'/images/logo.png',
|
||||
'/images/placeholder.png'
|
||||
]
|
||||
|
||||
// API endpoints to cache
|
||||
const CACHEABLE_API_PATTERNS = [
|
||||
/\/api\/v1\/users\/profile$/,
|
||||
/\/api\/v1\/campaigns\/templates$/,
|
||||
/\/api\/v1\/settings$/
|
||||
]
|
||||
|
||||
// Install event - cache static assets
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(STATIC_CACHE_NAME).then((cache) => {
|
||||
return cache.addAll(STATIC_ASSETS)
|
||||
})
|
||||
)
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
// Activate event - clean up old caches
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter((cacheName) => {
|
||||
return cacheName.startsWith('marketing-agent-') &&
|
||||
cacheName !== CACHE_NAME &&
|
||||
cacheName !== STATIC_CACHE_NAME &&
|
||||
cacheName !== DYNAMIC_CACHE_NAME &&
|
||||
cacheName !== API_CACHE_NAME
|
||||
})
|
||||
.map((cacheName) => caches.delete(cacheName))
|
||||
)
|
||||
})
|
||||
)
|
||||
self.clients.claim()
|
||||
})
|
||||
|
||||
// Fetch event - implement caching strategies
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event
|
||||
const url = new URL(request.url)
|
||||
|
||||
// Skip non-GET requests
|
||||
if (request.method !== 'GET') {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle API requests
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
event.respondWith(handleApiRequest(request))
|
||||
return
|
||||
}
|
||||
|
||||
// Handle static assets
|
||||
if (isStaticAsset(url.pathname)) {
|
||||
event.respondWith(handleStaticAsset(request))
|
||||
return
|
||||
}
|
||||
|
||||
// Handle dynamic content
|
||||
event.respondWith(handleDynamicContent(request))
|
||||
})
|
||||
|
||||
// Cache-first strategy for static assets
|
||||
async function handleStaticAsset(request) {
|
||||
const cache = await caches.open(STATIC_CACHE_NAME)
|
||||
const cached = await cache.match(request)
|
||||
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(request)
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone())
|
||||
}
|
||||
return response
|
||||
} catch (error) {
|
||||
return new Response('Offline - Asset not available', {
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Network-first strategy for API requests with fallback
|
||||
async function handleApiRequest(request) {
|
||||
const cache = await caches.open(API_CACHE_NAME)
|
||||
|
||||
try {
|
||||
const response = await fetch(request)
|
||||
|
||||
// Cache successful responses for cacheable endpoints
|
||||
if (response.ok && isCacheableApi(request.url)) {
|
||||
cache.put(request, response.clone())
|
||||
}
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
// Try cache on network failure
|
||||
const cached = await cache.match(request)
|
||||
if (cached) {
|
||||
// Add header to indicate cached response
|
||||
const headers = new Headers(cached.headers)
|
||||
headers.set('X-From-Cache', 'true')
|
||||
|
||||
return new Response(cached.body, {
|
||||
status: cached.status,
|
||||
statusText: cached.statusText,
|
||||
headers
|
||||
})
|
||||
}
|
||||
|
||||
// Return offline response
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Network unavailable',
|
||||
offline: true
|
||||
}),
|
||||
{
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Stale-while-revalidate strategy for dynamic content
|
||||
async function handleDynamicContent(request) {
|
||||
const cache = await caches.open(DYNAMIC_CACHE_NAME)
|
||||
const cached = await cache.match(request)
|
||||
|
||||
const fetchPromise = fetch(request).then((response) => {
|
||||
if (response.ok) {
|
||||
cache.put(request, response.clone())
|
||||
}
|
||||
return response
|
||||
})
|
||||
|
||||
return cached || fetchPromise
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function isStaticAsset(pathname) {
|
||||
return /\.(js|css|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$/.test(pathname)
|
||||
}
|
||||
|
||||
function isCacheableApi(url) {
|
||||
return CACHEABLE_API_PATTERNS.some(pattern => pattern.test(url))
|
||||
}
|
||||
|
||||
// Background sync for offline actions
|
||||
self.addEventListener('sync', (event) => {
|
||||
if (event.tag === 'sync-campaigns') {
|
||||
event.waitUntil(syncCampaigns())
|
||||
}
|
||||
})
|
||||
|
||||
async function syncCampaigns() {
|
||||
const cache = await caches.open('offline-campaigns')
|
||||
const requests = await cache.keys()
|
||||
|
||||
for (const request of requests) {
|
||||
try {
|
||||
const response = await fetch(request)
|
||||
if (response.ok) {
|
||||
await cache.delete(request)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to sync:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push notifications
|
||||
self.addEventListener('push', (event) => {
|
||||
const options = {
|
||||
body: event.data ? event.data.text() : 'New notification',
|
||||
icon: '/images/logo.png',
|
||||
badge: '/images/badge.png',
|
||||
vibrate: [100, 50, 100],
|
||||
data: {
|
||||
dateOfArrival: Date.now(),
|
||||
primaryKey: 1
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
action: 'view',
|
||||
title: 'View',
|
||||
icon: '/images/checkmark.png'
|
||||
},
|
||||
{
|
||||
action: 'close',
|
||||
title: 'Close',
|
||||
icon: '/images/xmark.png'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification('Marketing Agent', options)
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
event.notification.close()
|
||||
|
||||
if (event.action === 'view') {
|
||||
event.waitUntil(
|
||||
clients.openWindow('/')
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Message handling for cache control
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting()
|
||||
} else if (event.data.type === 'CLEAR_CACHE') {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => caches.delete(cacheName))
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
59
marketing-agent/frontend/public/test.html
Normal file
59
marketing-agent/frontend/public/test.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login Test</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Login Test</h1>
|
||||
<button onclick="testLogin()">Test Login</button>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
async function testLogin() {
|
||||
const resultDiv = document.getElementById('result');
|
||||
resultDiv.innerHTML = 'Testing login...';
|
||||
|
||||
try {
|
||||
const response = await axios.post('/api/v1/auth/login', {
|
||||
username: 'admin',
|
||||
password: 'admin123456'
|
||||
});
|
||||
|
||||
console.log('Response:', response.data);
|
||||
|
||||
if (response.data.success) {
|
||||
resultDiv.innerHTML = `
|
||||
<h3>Login Success!</h3>
|
||||
<p>Token: ${response.data.data.accessToken.substring(0, 50)}...</p>
|
||||
<p>User: ${response.data.data.user.username} (${response.data.data.user.role})</p>
|
||||
<button onclick="testDashboard('${response.data.data.accessToken}')">Test Dashboard API</button>
|
||||
`;
|
||||
} else {
|
||||
resultDiv.innerHTML = `<h3>Login Failed: ${response.data.error}</h3>`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `<h3>Error: ${error.message}</h3>`;
|
||||
console.error('Login error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function testDashboard(token) {
|
||||
try {
|
||||
const response = await axios.get('/api/v1/analytics/dashboard', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
console.log('Dashboard response:', response.data);
|
||||
alert('Dashboard API works! Check console for data.');
|
||||
} catch (error) {
|
||||
console.error('Dashboard error:', error);
|
||||
alert('Dashboard API error: ' + error.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
72
marketing-agent/frontend/public/verify-fix.html
Normal file
72
marketing-agent/frontend/public/verify-fix.html
Normal file
@@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Verify Fix</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; padding: 20px; }
|
||||
.step { margin: 20px 0; padding: 10px; border: 1px solid #ddd; }
|
||||
.success { color: green; }
|
||||
.error { color: red; }
|
||||
button { padding: 10px 20px; margin: 5px; cursor: pointer; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Verify Login Fix</h1>
|
||||
|
||||
<div class="step">
|
||||
<h3>Step 1: Clear Everything</h3>
|
||||
<button onclick="clearAll()">Clear Cache & Reload</button>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<h3>Step 2: Test Login</h3>
|
||||
<button onclick="testLogin()">Login as Admin</button>
|
||||
<div id="loginResult"></div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<h3>Step 3: Navigate to Dashboard</h3>
|
||||
<button onclick="goToDashboard()">Go to Dashboard</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function clearAll() {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
// Force reload to clear any cached modules
|
||||
location.href = location.href + '?t=' + Date.now();
|
||||
}
|
||||
|
||||
async function testLogin() {
|
||||
const resultDiv = document.getElementById('loginResult');
|
||||
try {
|
||||
const response = await fetch('/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: 'admin',
|
||||
password: 'admin123456'
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
localStorage.setItem('token', data.data.accessToken);
|
||||
localStorage.setItem('refreshToken', data.data.refreshToken);
|
||||
resultDiv.innerHTML = '<p class="success">✓ Login successful! Token saved.</p>';
|
||||
} else {
|
||||
resultDiv.innerHTML = '<p class="error">✗ Login failed: ' + data.error + '</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = '<p class="error">✗ Error: ' + error.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function goToDashboard() {
|
||||
window.location.href = '/';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
18
marketing-agent/frontend/src/App.vue
Normal file
18
marketing-agent/frontend/src/App.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<el-config-provider :locale="locale">
|
||||
<router-view />
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import en from 'element-plus/es/locale/lang/en'
|
||||
|
||||
const { locale: i18nLocale } = useI18n()
|
||||
|
||||
const locale = computed(() => {
|
||||
return i18nLocale.value === 'zh' ? zhCn : en
|
||||
})
|
||||
</script>
|
||||
289
marketing-agent/frontend/src/api/billing.js
Normal file
289
marketing-agent/frontend/src/api/billing.js
Normal file
@@ -0,0 +1,289 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// Subscriptions
|
||||
export function getSubscriptions() {
|
||||
return request({
|
||||
url: '/api/v1/billing/subscriptions',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getSubscription(id) {
|
||||
return request({
|
||||
url: `/api/v1/billing/subscriptions/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function createSubscription(data) {
|
||||
return request({
|
||||
url: '/api/v1/billing/subscriptions',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateSubscription(id, data) {
|
||||
return request({
|
||||
url: `/api/v1/billing/subscriptions/${id}`,
|
||||
method: 'patch',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function cancelSubscription(id, data) {
|
||||
return request({
|
||||
url: `/api/v1/billing/subscriptions/${id}/cancel`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function reactivateSubscription(id) {
|
||||
return request({
|
||||
url: `/api/v1/billing/subscriptions/${id}/reactivate`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
export function recordUsage(id, data) {
|
||||
return request({
|
||||
url: `/api/v1/billing/subscriptions/${id}/usage`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getSubscriptionUsage(id, params) {
|
||||
return request({
|
||||
url: `/api/v1/billing/subscriptions/${id}/usage`,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function applyDiscount(id, data) {
|
||||
return request({
|
||||
url: `/api/v1/billing/subscriptions/${id}/discount`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// Invoices
|
||||
export function getInvoices(params) {
|
||||
return request({
|
||||
url: '/api/v1/billing/invoices',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getInvoice(id) {
|
||||
return request({
|
||||
url: `/api/v1/billing/invoices/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function createInvoice(data) {
|
||||
return request({
|
||||
url: '/api/v1/billing/invoices',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateInvoice(id, data) {
|
||||
return request({
|
||||
url: `/api/v1/billing/invoices/${id}`,
|
||||
method: 'patch',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function finalizeInvoice(id) {
|
||||
return request({
|
||||
url: `/api/v1/billing/invoices/${id}/finalize`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
export function payInvoice(id, data) {
|
||||
return request({
|
||||
url: `/api/v1/billing/invoices/${id}/pay`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function voidInvoice(id, data) {
|
||||
return request({
|
||||
url: `/api/v1/billing/invoices/${id}/void`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function downloadInvoice(id) {
|
||||
return request({
|
||||
url: `/api/v1/billing/invoices/${id}/pdf`,
|
||||
method: 'get',
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
export function sendInvoiceReminder(id) {
|
||||
return request({
|
||||
url: `/api/v1/billing/invoices/${id}/remind`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
export function getUnpaidInvoices() {
|
||||
return request({
|
||||
url: '/api/v1/billing/invoices/unpaid',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getOverdueInvoices() {
|
||||
return request({
|
||||
url: '/api/v1/billing/invoices/overdue',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// Payment Methods
|
||||
export function getPaymentMethods() {
|
||||
return request({
|
||||
url: '/api/v1/billing/payment-methods',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getPaymentMethod(id) {
|
||||
return request({
|
||||
url: `/api/v1/billing/payment-methods/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function addPaymentMethod(data) {
|
||||
return request({
|
||||
url: '/api/v1/billing/payment-methods',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updatePaymentMethod(id, data) {
|
||||
return request({
|
||||
url: `/api/v1/billing/payment-methods/${id}`,
|
||||
method: 'patch',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function setDefaultPaymentMethod(id) {
|
||||
return request({
|
||||
url: `/api/v1/billing/payment-methods/${id}/default`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
export function removePaymentMethod(id) {
|
||||
return request({
|
||||
url: `/api/v1/billing/payment-methods/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function verifyPaymentMethod(id, data) {
|
||||
return request({
|
||||
url: `/api/v1/billing/payment-methods/${id}/verify`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// Transactions
|
||||
export function getTransactions(params) {
|
||||
return request({
|
||||
url: '/api/v1/billing/transactions',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getTransaction(id) {
|
||||
return request({
|
||||
url: `/api/v1/billing/transactions/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function createRefund(id, data) {
|
||||
return request({
|
||||
url: `/api/v1/billing/transactions/${id}/refund`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getTransactionSummary(period, params) {
|
||||
return request({
|
||||
url: `/api/v1/billing/transactions/summary/${period}`,
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function exportTransactions(format, params) {
|
||||
return request({
|
||||
url: `/api/v1/billing/transactions/export/${format}`,
|
||||
method: 'get',
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
export function createAdjustment(data) {
|
||||
return request({
|
||||
url: '/api/v1/billing/transactions/adjustment',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// Plans
|
||||
export function getPlans() {
|
||||
return request({
|
||||
url: '/api/v1/billing/plans',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getPlan(id) {
|
||||
return request({
|
||||
url: `/api/v1/billing/plans/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// Coupons
|
||||
export function validateCoupon(code) {
|
||||
return request({
|
||||
url: '/api/v1/billing/coupons/validate',
|
||||
method: 'post',
|
||||
data: { code }
|
||||
})
|
||||
}
|
||||
|
||||
// Stripe Customer Portal
|
||||
export function createCustomerPortalSession() {
|
||||
return request({
|
||||
url: '/api/v1/billing/customer-portal',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
102
marketing-agent/frontend/src/api/index.js
Normal file
102
marketing-agent/frontend/src/api/index.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import router from '@/router'
|
||||
|
||||
// Create axios instance
|
||||
const request = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// Request interceptor
|
||||
request.interceptors.request.use(
|
||||
config => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
console.error('Request error:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// Response interceptor
|
||||
request.interceptors.response.use(
|
||||
response => {
|
||||
console.log('API Response:', response.config.url, response.data)
|
||||
return response
|
||||
},
|
||||
error => {
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
localStorage.removeItem('token')
|
||||
router.push({ name: 'Login' })
|
||||
ElMessage.error('Authentication expired, please login again')
|
||||
break
|
||||
case 403:
|
||||
ElMessage.error('Access denied')
|
||||
break
|
||||
case 404:
|
||||
ElMessage.error('Resource not found')
|
||||
break
|
||||
case 429:
|
||||
ElMessage.error('Too many requests, please try again later')
|
||||
break
|
||||
case 500:
|
||||
ElMessage.error('Server error, please try again later')
|
||||
break
|
||||
default:
|
||||
ElMessage.error(error.response.data?.error || 'Operation failed')
|
||||
}
|
||||
} else if (error.request) {
|
||||
ElMessage.error('Network error, please check your connection')
|
||||
} else {
|
||||
ElMessage.error('Request failed')
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// API modules
|
||||
import auth from './modules/auth'
|
||||
import campaigns from './modules/campaigns'
|
||||
import analytics from './modules/analytics'
|
||||
import abTesting from './modules/abTesting'
|
||||
import accounts from './modules/accounts'
|
||||
import compliance from './modules/compliance'
|
||||
import ai from './modules/ai'
|
||||
import settings from './modules/settings'
|
||||
import scheduledCampaigns from './modules/scheduledCampaigns'
|
||||
import segments from './modules/segments'
|
||||
import templates from './modules/templates'
|
||||
|
||||
const api = {
|
||||
auth,
|
||||
campaigns,
|
||||
analytics,
|
||||
abTesting,
|
||||
accounts,
|
||||
compliance,
|
||||
ai,
|
||||
settings,
|
||||
scheduledCampaigns,
|
||||
segments,
|
||||
templates,
|
||||
setAuthToken(token) {
|
||||
if (token) {
|
||||
request.defaults.headers.common['Authorization'] = `Bearer ${token}`
|
||||
} else {
|
||||
delete request.defaults.headers.common['Authorization']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { request }
|
||||
export default api
|
||||
71
marketing-agent/frontend/src/api/modules/abTesting.js
Normal file
71
marketing-agent/frontend/src/api/modules/abTesting.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import { request } from '../index'
|
||||
|
||||
export default {
|
||||
// Experiments
|
||||
getExperiments(params) {
|
||||
return request.get('/ab-testing/experiments', { params })
|
||||
},
|
||||
|
||||
getExperiment(id) {
|
||||
return request.get(`/ab-testing/experiments/${id}`)
|
||||
},
|
||||
|
||||
createExperiment(data) {
|
||||
return request.post('/ab-testing/experiments', data)
|
||||
},
|
||||
|
||||
updateExperiment(id, data) {
|
||||
return request.put(`/ab-testing/experiments/${id}`, data)
|
||||
},
|
||||
|
||||
deleteExperiment(id) {
|
||||
return request.delete(`/ab-testing/experiments/${id}`)
|
||||
},
|
||||
|
||||
// Experiment actions
|
||||
startExperiment(id) {
|
||||
return request.post(`/ab-testing/experiments/${id}/start`)
|
||||
},
|
||||
|
||||
pauseExperiment(id) {
|
||||
return request.post(`/ab-testing/experiments/${id}/pause`)
|
||||
},
|
||||
|
||||
stopExperiment(id) {
|
||||
return request.post(`/ab-testing/experiments/${id}/stop`)
|
||||
},
|
||||
|
||||
// Variants
|
||||
getVariants(experimentId) {
|
||||
return request.get(`/ab-testing/experiments/${experimentId}/variants`)
|
||||
},
|
||||
|
||||
createVariant(experimentId, data) {
|
||||
return request.post(`/ab-testing/experiments/${experimentId}/variants`, data)
|
||||
},
|
||||
|
||||
updateVariant(experimentId, variantId, data) {
|
||||
return request.put(`/ab-testing/experiments/${experimentId}/variants/${variantId}`, data)
|
||||
},
|
||||
|
||||
deleteVariant(experimentId, variantId) {
|
||||
return request.delete(`/ab-testing/experiments/${experimentId}/variants/${variantId}`)
|
||||
},
|
||||
|
||||
// Results
|
||||
getResults(experimentId) {
|
||||
return request.get(`/ab-testing/experiments/${experimentId}/results`)
|
||||
},
|
||||
|
||||
// Significance test
|
||||
runSignificanceTest(experimentId) {
|
||||
return request.post(`/ab-testing/experiments/${experimentId}/significance-test`)
|
||||
},
|
||||
|
||||
// Winner selection
|
||||
selectWinner(experimentId, variantId) {
|
||||
return request.post(`/ab-testing/experiments/${experimentId}/select-winner`, {
|
||||
variantId
|
||||
})
|
||||
}
|
||||
}
|
||||
104
marketing-agent/frontend/src/api/modules/accounts.js
Normal file
104
marketing-agent/frontend/src/api/modules/accounts.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import { request } from '../index'
|
||||
|
||||
export default {
|
||||
// Get accounts list
|
||||
getList(params) {
|
||||
return request.get('/accounts', { params })
|
||||
},
|
||||
|
||||
// Telegram accounts
|
||||
getAccounts(params) {
|
||||
return request.get('/gramjs-adapter/accounts', { params })
|
||||
},
|
||||
|
||||
// Connect new Telegram account
|
||||
connectAccount(data) {
|
||||
return request.post('/gramjs-adapter/accounts/connect', data)
|
||||
},
|
||||
|
||||
// Verify account with code
|
||||
verifyAccount(accountId, data) {
|
||||
return request.post(`/gramjs-adapter/accounts/${accountId}/verify`, data)
|
||||
},
|
||||
|
||||
// Get account connection status
|
||||
getAccountStatus(accountId) {
|
||||
return request.get(`/gramjs-adapter/accounts/${accountId}/status`)
|
||||
},
|
||||
|
||||
// Disconnect account
|
||||
disconnectAccount(accountId) {
|
||||
return request.delete(`/gramjs-adapter/accounts/${accountId}`)
|
||||
},
|
||||
|
||||
// Reconnect account
|
||||
reconnectAccount(accountId) {
|
||||
return request.post(`/gramjs-adapter/accounts/${accountId}/reconnect`)
|
||||
},
|
||||
|
||||
getAccount(id) {
|
||||
return request.get(`/accounts/${id}`)
|
||||
},
|
||||
|
||||
addAccount(data) {
|
||||
return request.post('/accounts', data)
|
||||
},
|
||||
|
||||
updateAccount(id, data) {
|
||||
return request.put(`/accounts/${id}`, data)
|
||||
},
|
||||
|
||||
deleteAccount(id) {
|
||||
return request.delete(`/accounts/${id}`)
|
||||
},
|
||||
|
||||
// Update account status
|
||||
updateStatus(id, status) {
|
||||
return request.put(`/accounts/${id}/status`, { status })
|
||||
},
|
||||
|
||||
// Account actions
|
||||
activateAccount(id) {
|
||||
return request.post(`/accounts/${id}/activate`)
|
||||
},
|
||||
|
||||
deactivateAccount(id) {
|
||||
return request.post(`/accounts/${id}/deactivate`)
|
||||
},
|
||||
|
||||
refreshSession(id) {
|
||||
return request.post(`/accounts/${id}/refresh-session`)
|
||||
},
|
||||
|
||||
// Groups
|
||||
getGroups(params) {
|
||||
return request.get('/groups', { params })
|
||||
},
|
||||
|
||||
getGroup(id) {
|
||||
return request.get(`/groups/${id}`)
|
||||
},
|
||||
|
||||
syncGroups(accountId) {
|
||||
return request.post(`/accounts/${accountId}/sync-groups`)
|
||||
},
|
||||
|
||||
// Group members
|
||||
getGroupMembers(groupId, params) {
|
||||
return request.get(`/groups/${groupId}/members`, { params })
|
||||
},
|
||||
|
||||
// Account statistics
|
||||
getAccountStats(id) {
|
||||
return request.get(`/accounts/${id}/stats`)
|
||||
},
|
||||
|
||||
// Batch operations
|
||||
batchActivate(accountIds) {
|
||||
return request.post('/accounts/batch/activate', { accountIds })
|
||||
},
|
||||
|
||||
batchDeactivate(accountIds) {
|
||||
return request.post('/accounts/batch/deactivate', { accountIds })
|
||||
}
|
||||
}
|
||||
50
marketing-agent/frontend/src/api/modules/ai.js
Normal file
50
marketing-agent/frontend/src/api/modules/ai.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { request } from '../index'
|
||||
|
||||
export default {
|
||||
// Strategy generation
|
||||
generateStrategy(data) {
|
||||
return request.post('/claude/strategy/generate', data)
|
||||
},
|
||||
|
||||
// Campaign analysis
|
||||
analyzeCampaign(data) {
|
||||
return request.post('/claude/analysis/campaign', data)
|
||||
},
|
||||
|
||||
// Content generation
|
||||
generateContent(data) {
|
||||
return request.post('/claude/content/generate', data)
|
||||
},
|
||||
|
||||
optimizeContent(data) {
|
||||
return request.post('/claude/content/optimize', data)
|
||||
},
|
||||
|
||||
// Audience analysis
|
||||
analyzeAudience(data) {
|
||||
return request.post('/claude/analysis/audience', data)
|
||||
},
|
||||
|
||||
// Predictions
|
||||
predictPerformance(data) {
|
||||
return request.post('/claude/predict/performance', data)
|
||||
},
|
||||
|
||||
predictEngagement(data) {
|
||||
return request.post('/claude/predict/engagement', data)
|
||||
},
|
||||
|
||||
// Recommendations
|
||||
getRecommendations(type, params) {
|
||||
return request.get(`/claude/recommendations/${type}`, { params })
|
||||
},
|
||||
|
||||
// Chat interface
|
||||
sendMessage(data) {
|
||||
return request.post('/claude/chat', data)
|
||||
},
|
||||
|
||||
getChatHistory(sessionId) {
|
||||
return request.get(`/claude/chat/history/${sessionId}`)
|
||||
}
|
||||
}
|
||||
63
marketing-agent/frontend/src/api/modules/analytics.js
Normal file
63
marketing-agent/frontend/src/api/modules/analytics.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { request } from '../index'
|
||||
|
||||
export default {
|
||||
// Dashboard metrics
|
||||
getDashboardMetrics(params) {
|
||||
return request.get('/analytics/dashboard', { params })
|
||||
},
|
||||
|
||||
// Campaign analytics
|
||||
getCampaignMetrics(campaignId, params) {
|
||||
return request.get(`/analytics/campaigns/${campaignId}/metrics`, { params })
|
||||
},
|
||||
|
||||
// Message analytics
|
||||
getMessageMetrics(params) {
|
||||
return request.get('/analytics/messages', { params })
|
||||
},
|
||||
|
||||
// Engagement analytics
|
||||
getEngagementMetrics(params) {
|
||||
return request.get('/analytics/engagement', { params })
|
||||
},
|
||||
|
||||
// Conversion analytics
|
||||
getConversionMetrics(params) {
|
||||
return request.get('/analytics/conversions', { params })
|
||||
},
|
||||
|
||||
// Real-time analytics
|
||||
getRealTimeMetrics() {
|
||||
return request.get('/analytics/realtime')
|
||||
},
|
||||
|
||||
// Reports
|
||||
generateReport(data) {
|
||||
return request.post('/analytics/reports', data)
|
||||
},
|
||||
|
||||
getReports(params) {
|
||||
return request.get('/analytics/reports', { params })
|
||||
},
|
||||
|
||||
downloadReport(id) {
|
||||
return request.get(`/analytics/reports/${id}/download`, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
},
|
||||
|
||||
// Custom metrics
|
||||
trackEvent(data) {
|
||||
return request.post('/analytics/events', data)
|
||||
},
|
||||
|
||||
// Funnel analytics
|
||||
getFunnelMetrics(params) {
|
||||
return request.get('/analytics/funnel', { params })
|
||||
},
|
||||
|
||||
// Cohort analytics
|
||||
getCohortAnalysis(params) {
|
||||
return request.get('/analytics/cohorts', { params })
|
||||
}
|
||||
}
|
||||
40
marketing-agent/frontend/src/api/modules/auth.js
Normal file
40
marketing-agent/frontend/src/api/modules/auth.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { request } from '../index'
|
||||
|
||||
export default {
|
||||
login(data) {
|
||||
return request.post('/auth/login', data)
|
||||
},
|
||||
|
||||
register(data) {
|
||||
return request.post('/auth/register', data)
|
||||
},
|
||||
|
||||
logout() {
|
||||
return request.post('/auth/logout')
|
||||
},
|
||||
|
||||
getProfile() {
|
||||
return request.get('/auth/me')
|
||||
},
|
||||
|
||||
updateProfile(data) {
|
||||
return request.put('/auth/profile', data)
|
||||
},
|
||||
|
||||
changePassword(data) {
|
||||
return request.post('/auth/change-password', data)
|
||||
},
|
||||
|
||||
// API Key management
|
||||
getApiKeys() {
|
||||
return request.get('/auth/api-keys')
|
||||
},
|
||||
|
||||
createApiKey(data) {
|
||||
return request.post('/auth/api-keys', data)
|
||||
},
|
||||
|
||||
deleteApiKey(id) {
|
||||
return request.delete(`/auth/api-keys/${id}`)
|
||||
}
|
||||
}
|
||||
85
marketing-agent/frontend/src/api/modules/campaigns.js
Normal file
85
marketing-agent/frontend/src/api/modules/campaigns.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import { request } from '../index'
|
||||
|
||||
export default {
|
||||
// Campaign CRUD
|
||||
getList(params) {
|
||||
return request.get('/orchestrator/campaigns', { params })
|
||||
},
|
||||
|
||||
getDetail(id) {
|
||||
return request.get(`/orchestrator/campaigns/${id}`)
|
||||
},
|
||||
|
||||
create(data) {
|
||||
return request.post('/orchestrator/campaigns', data)
|
||||
},
|
||||
|
||||
update(id, data) {
|
||||
return request.put(`/orchestrator/campaigns/${id}`, data)
|
||||
},
|
||||
|
||||
delete(id) {
|
||||
return request.delete(`/orchestrator/campaigns/${id}`)
|
||||
},
|
||||
|
||||
// Campaign actions
|
||||
execute(id) {
|
||||
return request.post(`/orchestrator/campaigns/${id}/execute`)
|
||||
},
|
||||
|
||||
pause(id) {
|
||||
return request.post(`/orchestrator/campaigns/${id}/pause`)
|
||||
},
|
||||
|
||||
resume(id) {
|
||||
return request.post(`/orchestrator/campaigns/${id}/resume`)
|
||||
},
|
||||
|
||||
cancel(id) {
|
||||
return request.post(`/orchestrator/campaigns/${id}/cancel`)
|
||||
},
|
||||
|
||||
clone(id) {
|
||||
return request.post(`/orchestrator/campaigns/${id}/clone`)
|
||||
},
|
||||
|
||||
// Campaign progress
|
||||
getProgress(id) {
|
||||
return request.get(`/orchestrator/campaigns/${id}/progress`)
|
||||
},
|
||||
|
||||
// Campaign statistics
|
||||
getStatistics(id) {
|
||||
return request.get(`/orchestrator/campaigns/${id}/statistics`)
|
||||
},
|
||||
|
||||
// Campaign messages
|
||||
getMessages(id, params) {
|
||||
return request.get(`/orchestrator/campaigns/${id}/messages`, { params })
|
||||
},
|
||||
|
||||
// Message templates
|
||||
getTemplates() {
|
||||
return request.get('/orchestrator/messages/templates')
|
||||
},
|
||||
|
||||
getTemplate(id) {
|
||||
return request.get(`/orchestrator/messages/templates/${id}`)
|
||||
},
|
||||
|
||||
createTemplate(data) {
|
||||
return request.post('/orchestrator/messages/templates', data)
|
||||
},
|
||||
|
||||
updateTemplate(id, data) {
|
||||
return request.put(`/orchestrator/messages/templates/${id}`, data)
|
||||
},
|
||||
|
||||
deleteTemplate(id) {
|
||||
return request.delete(`/orchestrator/messages/templates/${id}`)
|
||||
},
|
||||
|
||||
previewTemplate(id, variables) {
|
||||
return request.post(`/orchestrator/messages/templates/${id}/preview`, { variables })
|
||||
}
|
||||
}
|
||||
71
marketing-agent/frontend/src/api/modules/compliance.js
Normal file
71
marketing-agent/frontend/src/api/modules/compliance.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import { request } from '../index'
|
||||
|
||||
export default {
|
||||
// Consent management
|
||||
getConsent(userId) {
|
||||
return request.get(`/compliance/consent/${userId}`)
|
||||
},
|
||||
|
||||
updateConsent(userId, data) {
|
||||
return request.put(`/compliance/consent/${userId}`, data)
|
||||
},
|
||||
|
||||
recordConsent(data) {
|
||||
return request.post('/compliance/consent/record', data)
|
||||
},
|
||||
|
||||
// Privacy rights
|
||||
requestDataExport(userId) {
|
||||
return request.post('/compliance/privacy/export', { userId })
|
||||
},
|
||||
|
||||
requestDataDeletion(userId, data) {
|
||||
return request.post('/compliance/privacy/delete', {
|
||||
userId,
|
||||
...data
|
||||
})
|
||||
},
|
||||
|
||||
getPrivacyRequests(params) {
|
||||
return request.get('/compliance/privacy/requests', { params })
|
||||
},
|
||||
|
||||
// Audit logs
|
||||
getAuditLogs(params) {
|
||||
return request.get('/compliance/audit/logs', { params })
|
||||
},
|
||||
|
||||
generateComplianceReport(data) {
|
||||
return request.post('/compliance/audit/report', data)
|
||||
},
|
||||
|
||||
// Regulatory compliance
|
||||
getGDPRStatus() {
|
||||
return request.get('/compliance/regulatory/gdpr/status')
|
||||
},
|
||||
|
||||
getCCPAStatus() {
|
||||
return request.get('/compliance/regulatory/ccpa/status')
|
||||
},
|
||||
|
||||
// Data retention
|
||||
getRetentionPolicies() {
|
||||
return request.get('/compliance/retention/policies')
|
||||
},
|
||||
|
||||
updateRetentionPolicy(type, data) {
|
||||
return request.put(`/compliance/retention/policies/${type}`, data)
|
||||
},
|
||||
|
||||
// Do Not Sell
|
||||
getDoNotSellStatus(userId) {
|
||||
return request.get(`/compliance/privacy/donotsell/${userId}`)
|
||||
},
|
||||
|
||||
updateDoNotSellStatus(userId, optOut) {
|
||||
return request.post('/compliance/privacy/donotsell', {
|
||||
userId,
|
||||
optOut
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { request } from '../index'
|
||||
|
||||
export default {
|
||||
// Get all scheduled campaigns
|
||||
getAll(params) {
|
||||
return request.get('/scheduled-campaigns', { params })
|
||||
},
|
||||
|
||||
// Get scheduled campaign by ID
|
||||
get(id) {
|
||||
return request.get(`/scheduled-campaigns/${id}`)
|
||||
},
|
||||
|
||||
// Create scheduled campaign
|
||||
create(data) {
|
||||
return request.post('/scheduled-campaigns', data)
|
||||
},
|
||||
|
||||
// Update scheduled campaign
|
||||
update(id, data) {
|
||||
return request.put(`/scheduled-campaigns/${id}`, data)
|
||||
},
|
||||
|
||||
// Delete scheduled campaign
|
||||
delete(id) {
|
||||
return request.delete(`/scheduled-campaigns/${id}`)
|
||||
},
|
||||
|
||||
// Get campaign history
|
||||
getHistory(id, limit = 50) {
|
||||
return request.get(`/scheduled-campaigns/${id}/history`, {
|
||||
params: { limit }
|
||||
})
|
||||
},
|
||||
|
||||
// Pause campaign
|
||||
pause(id) {
|
||||
return request.post(`/scheduled-campaigns/${id}/pause`)
|
||||
},
|
||||
|
||||
// Resume campaign
|
||||
resume(id) {
|
||||
return request.post(`/scheduled-campaigns/${id}/resume`)
|
||||
},
|
||||
|
||||
// Test campaign
|
||||
test(id, options) {
|
||||
return request.post(`/scheduled-campaigns/${id}/test`, options)
|
||||
},
|
||||
|
||||
// Get statistics
|
||||
getStatistics(period = '7d') {
|
||||
return request.get(`/scheduled-campaigns/statistics/${period}`)
|
||||
}
|
||||
}
|
||||
55
marketing-agent/frontend/src/api/modules/segments.js
Normal file
55
marketing-agent/frontend/src/api/modules/segments.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { request } from '../index'
|
||||
|
||||
export default {
|
||||
// Get all segments
|
||||
getAll(params) {
|
||||
return request.get('/segments', { params })
|
||||
},
|
||||
|
||||
// Get segment by ID
|
||||
get(id) {
|
||||
return request.get(`/segments/${id}`)
|
||||
},
|
||||
|
||||
// Create segment
|
||||
create(data) {
|
||||
return request.post('/segments', data)
|
||||
},
|
||||
|
||||
// Update segment
|
||||
update(id, data) {
|
||||
return request.put(`/segments/${id}`, data)
|
||||
},
|
||||
|
||||
// Delete segment
|
||||
delete(id) {
|
||||
return request.delete(`/segments/${id}`)
|
||||
},
|
||||
|
||||
// Test segment
|
||||
test(id) {
|
||||
return request.post(`/segments/${id}/test`)
|
||||
},
|
||||
|
||||
// Get segment users
|
||||
getUsers(id, params) {
|
||||
return request.get(`/segments/${id}/users`, { params })
|
||||
},
|
||||
|
||||
// Export segment users
|
||||
export(id) {
|
||||
return request.get(`/segments/${id}/export`, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
},
|
||||
|
||||
// Clone segment
|
||||
clone(id, data) {
|
||||
return request.post(`/segments/${id}/clone`, data)
|
||||
},
|
||||
|
||||
// Get segment statistics
|
||||
getStats() {
|
||||
return request.get('/segments-stats')
|
||||
}
|
||||
}
|
||||
45
marketing-agent/frontend/src/api/modules/settings.js
Normal file
45
marketing-agent/frontend/src/api/modules/settings.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import request from '../index'
|
||||
|
||||
export default {
|
||||
// Get user settings
|
||||
get() {
|
||||
return request({
|
||||
url: '/settings',
|
||||
method: 'get'
|
||||
})
|
||||
},
|
||||
|
||||
// Update user settings
|
||||
update(data) {
|
||||
return request({
|
||||
url: '/settings',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// Get API keys
|
||||
getApiKeys() {
|
||||
return request({
|
||||
url: '/settings/api-keys',
|
||||
method: 'get'
|
||||
})
|
||||
},
|
||||
|
||||
// Generate new API key
|
||||
generateApiKey(data) {
|
||||
return request({
|
||||
url: '/settings/api-keys',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// Delete API key
|
||||
deleteApiKey(id) {
|
||||
return request({
|
||||
url: `/settings/api-keys/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
}
|
||||
53
marketing-agent/frontend/src/api/modules/templates.js
Normal file
53
marketing-agent/frontend/src/api/modules/templates.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { request } from '../index'
|
||||
|
||||
export default {
|
||||
// Get all templates
|
||||
getAll(params) {
|
||||
return request.get('/templates', { params })
|
||||
},
|
||||
|
||||
// Get template by ID
|
||||
get(id) {
|
||||
return request.get(`/templates/${id}`)
|
||||
},
|
||||
|
||||
// Create template
|
||||
create(data) {
|
||||
return request.post('/templates', data)
|
||||
},
|
||||
|
||||
// Update template
|
||||
update(id, data) {
|
||||
return request.put(`/templates/${id}`, data)
|
||||
},
|
||||
|
||||
// Delete template
|
||||
delete(id) {
|
||||
return request.delete(`/templates/${id}`)
|
||||
},
|
||||
|
||||
// Preview template
|
||||
preview(id, data) {
|
||||
return request.post(`/templates/${id}/preview`, data)
|
||||
},
|
||||
|
||||
// Test template
|
||||
test(id, data) {
|
||||
return request.post(`/templates/${id}/test`, data)
|
||||
},
|
||||
|
||||
// Clone template
|
||||
clone(id, data) {
|
||||
return request.post(`/templates/${id}/clone`, data)
|
||||
},
|
||||
|
||||
// Get template categories
|
||||
getCategories() {
|
||||
return request.get('/template-categories')
|
||||
},
|
||||
|
||||
// Get template variables
|
||||
getVariables() {
|
||||
return request.get('/template-variables')
|
||||
}
|
||||
}
|
||||
166
marketing-agent/frontend/src/api/tenant.js
Normal file
166
marketing-agent/frontend/src/api/tenant.js
Normal file
@@ -0,0 +1,166 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export const tenantApi = {
|
||||
// Get current tenant information
|
||||
getCurrent() {
|
||||
return request({
|
||||
url: '/api/v1/tenants/current',
|
||||
method: 'get'
|
||||
})
|
||||
},
|
||||
|
||||
// Update tenant basic information
|
||||
update(data) {
|
||||
return request({
|
||||
url: '/api/v1/tenants/current',
|
||||
method: 'patch',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// Update tenant settings
|
||||
updateSettings(settings) {
|
||||
return request({
|
||||
url: '/api/v1/tenants/current/settings',
|
||||
method: 'patch',
|
||||
data: { settings }
|
||||
})
|
||||
},
|
||||
|
||||
// Update tenant branding
|
||||
updateBranding(branding) {
|
||||
return request({
|
||||
url: '/api/v1/tenants/current/branding',
|
||||
method: 'patch',
|
||||
data: { branding }
|
||||
})
|
||||
},
|
||||
|
||||
// Update tenant compliance settings
|
||||
updateCompliance(compliance) {
|
||||
return request({
|
||||
url: '/api/v1/tenants/current/compliance',
|
||||
method: 'patch',
|
||||
data: { compliance }
|
||||
})
|
||||
},
|
||||
|
||||
// Get tenant usage statistics
|
||||
getUsage() {
|
||||
return request({
|
||||
url: '/api/v1/tenants/current/usage',
|
||||
method: 'get'
|
||||
})
|
||||
},
|
||||
|
||||
// Get tenant billing information
|
||||
getBilling() {
|
||||
return request({
|
||||
url: '/api/v1/tenants/current/billing',
|
||||
method: 'get'
|
||||
})
|
||||
},
|
||||
|
||||
// List all tenants (superadmin only)
|
||||
list(params) {
|
||||
return request({
|
||||
url: '/api/v1/tenants',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
},
|
||||
|
||||
// Get tenant by ID (superadmin only)
|
||||
getById(id) {
|
||||
return request({
|
||||
url: `/api/v1/tenants/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
},
|
||||
|
||||
// Update tenant by ID (superadmin only)
|
||||
updateById(id, data) {
|
||||
return request({
|
||||
url: `/api/v1/tenants/${id}`,
|
||||
method: 'patch',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// Delete tenant (superadmin only)
|
||||
delete(id) {
|
||||
return request({
|
||||
url: `/api/v1/tenants/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
},
|
||||
|
||||
// Create new tenant (public)
|
||||
signup(data) {
|
||||
return request({
|
||||
url: '/api/v1/tenants/signup',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
},
|
||||
|
||||
// Check if slug is available
|
||||
checkSlug(slug) {
|
||||
return request({
|
||||
url: '/api/v1/tenants/check-slug',
|
||||
method: 'get',
|
||||
params: { slug }
|
||||
})
|
||||
},
|
||||
|
||||
// Upload tenant logo
|
||||
uploadLogo(file) {
|
||||
const formData = new FormData()
|
||||
formData.append('logo', file)
|
||||
|
||||
return request({
|
||||
url: '/api/v1/tenants/current/branding/logo',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// Get tenant features
|
||||
getFeatures() {
|
||||
return request({
|
||||
url: '/api/v1/tenants/current/features',
|
||||
method: 'get'
|
||||
})
|
||||
},
|
||||
|
||||
// Upgrade tenant plan
|
||||
upgradePlan(plan, paymentMethod) {
|
||||
return request({
|
||||
url: '/api/v1/tenants/current/upgrade',
|
||||
method: 'post',
|
||||
data: { plan, paymentMethod }
|
||||
})
|
||||
},
|
||||
|
||||
// Get tenant audit logs
|
||||
getAuditLogs(params) {
|
||||
return request({
|
||||
url: '/api/v1/tenants/current/audit-logs',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
},
|
||||
|
||||
// Export tenant data
|
||||
exportData(format = 'json') {
|
||||
return request({
|
||||
url: '/api/v1/tenants/current/export',
|
||||
method: 'get',
|
||||
params: { format },
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
}
|
||||
88
marketing-agent/frontend/src/components/AnimatedNumber.vue
Normal file
88
marketing-agent/frontend/src/components/AnimatedNumber.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<span class="animated-number">{{ displayValue }}</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed, onMounted } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
format: {
|
||||
type: Function,
|
||||
default: (val) => val.toString()
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 1000
|
||||
},
|
||||
easing: {
|
||||
type: String,
|
||||
default: 'easeOutQuart'
|
||||
}
|
||||
})
|
||||
|
||||
const currentValue = ref(0)
|
||||
let animationFrame = null
|
||||
|
||||
const displayValue = computed(() => {
|
||||
return props.format(currentValue.value)
|
||||
})
|
||||
|
||||
// Easing functions
|
||||
const easingFunctions = {
|
||||
linear: (t) => t,
|
||||
easeOutQuart: (t) => 1 - Math.pow(1 - t, 4),
|
||||
easeInOutQuart: (t) => t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2
|
||||
}
|
||||
|
||||
const animate = (fromValue, toValue) => {
|
||||
const startTime = Date.now()
|
||||
const endTime = startTime + props.duration
|
||||
const easingFunction = easingFunctions[props.easing] || easingFunctions.easeOutQuart
|
||||
|
||||
const update = () => {
|
||||
const now = Date.now()
|
||||
const progress = Math.min((now - startTime) / props.duration, 1)
|
||||
const easedProgress = easingFunction(progress)
|
||||
|
||||
currentValue.value = fromValue + (toValue - fromValue) * easedProgress
|
||||
|
||||
if (progress < 1) {
|
||||
animationFrame = requestAnimationFrame(update)
|
||||
} else {
|
||||
currentValue.value = toValue
|
||||
}
|
||||
}
|
||||
|
||||
if (animationFrame) {
|
||||
cancelAnimationFrame(animationFrame)
|
||||
}
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
watch(() => props.value, (newValue, oldValue) => {
|
||||
animate(oldValue || 0, newValue)
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
currentValue.value = props.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.animated-number {
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&.increasing {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.decreasing {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<div class="performance-monitor">
|
||||
<el-card class="monitor-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>Performance Metrics</span>
|
||||
<el-button type="primary" size="small" @click="refreshMetrics">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
Refresh
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="metrics-grid">
|
||||
<!-- Core Web Vitals -->
|
||||
<div class="metric-card" :class="getMetricClass(metrics.lcp, lcpThresholds)">
|
||||
<div class="metric-label">Largest Contentful Paint (LCP)</div>
|
||||
<div class="metric-value">{{ formatTime(metrics.lcp) }}</div>
|
||||
<div class="metric-target">Target: < 2.5s</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card" :class="getMetricClass(metrics.fid, fidThresholds)">
|
||||
<div class="metric-label">First Input Delay (FID)</div>
|
||||
<div class="metric-value">{{ formatTime(metrics.fid, 'ms') }}</div>
|
||||
<div class="metric-target">Target: < 100ms</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card" :class="getMetricClass(metrics.cls, clsThresholds)">
|
||||
<div class="metric-label">Cumulative Layout Shift (CLS)</div>
|
||||
<div class="metric-value">{{ metrics.cls?.toFixed(3) || 'N/A' }}</div>
|
||||
<div class="metric-target">Target: < 0.1</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card" :class="getMetricClass(metrics.fcp, fcpThresholds)">
|
||||
<div class="metric-label">First Contentful Paint (FCP)</div>
|
||||
<div class="metric-value">{{ formatTime(metrics.fcp) }}</div>
|
||||
<div class="metric-target">Target: < 1.8s</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Time to First Byte (TTFB)</div>
|
||||
<div class="metric-value">{{ formatTime(metrics.ttfb, 'ms') }}</div>
|
||||
<div class="metric-target">Target: < 800ms</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Time to Interactive (TTI)</div>
|
||||
<div class="metric-value">{{ formatTime(metrics.tti) }}</div>
|
||||
<div class="metric-target">Target: < 3.8s</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory Usage -->
|
||||
<div class="section-title">Memory Usage</div>
|
||||
<div class="memory-stats" v-if="memoryStats">
|
||||
<el-progress
|
||||
:percentage="memoryUsagePercentage"
|
||||
:color="getProgressColor(memoryUsagePercentage)"
|
||||
:stroke-width="20"
|
||||
text-inside
|
||||
>
|
||||
<span>{{ formatBytes(memoryStats.usedJSHeapSize) }} / {{ formatBytes(memoryStats.jsHeapSizeLimit) }}</span>
|
||||
</el-progress>
|
||||
</div>
|
||||
|
||||
<!-- Resource Timing -->
|
||||
<div class="section-title">Resource Load Times</div>
|
||||
<el-table
|
||||
:data="resourceTimings"
|
||||
size="small"
|
||||
max-height="300"
|
||||
>
|
||||
<el-table-column prop="name" label="Resource" width="300">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="row.fullName" placement="top">
|
||||
<span class="resource-name">{{ row.name }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="Type" width="100" />
|
||||
<el-table-column prop="size" label="Size" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ formatBytes(row.size) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="duration" label="Duration" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.duration }}ms
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Timeline" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="timeline">
|
||||
<div
|
||||
class="timeline-bar"
|
||||
:style="{
|
||||
left: `${(row.startTime / maxTime) * 100}%`,
|
||||
width: `${(row.duration / maxTime) * 100}%`,
|
||||
backgroundColor: getResourceColor(row.type)
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="actions">
|
||||
<el-button @click="clearCache">Clear Cache</el-button>
|
||||
<el-button @click="runLighthouse">Run Lighthouse</el-button>
|
||||
<el-button type="primary" @click="exportReport">Export Report</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import { performanceMonitor } from '@/utils/performance'
|
||||
import { clearAllCaches } from '@/utils/serviceWorker'
|
||||
|
||||
const metrics = ref({
|
||||
fcp: null,
|
||||
lcp: null,
|
||||
fid: null,
|
||||
cls: null,
|
||||
ttfb: null,
|
||||
tti: null
|
||||
})
|
||||
|
||||
const memoryStats = ref(null)
|
||||
const resourceTimings = ref([])
|
||||
|
||||
// Thresholds for Core Web Vitals
|
||||
const lcpThresholds = { good: 2500, needsImprovement: 4000 }
|
||||
const fidThresholds = { good: 100, needsImprovement: 300 }
|
||||
const clsThresholds = { good: 0.1, needsImprovement: 0.25 }
|
||||
const fcpThresholds = { good: 1800, needsImprovement: 3000 }
|
||||
|
||||
const memoryUsagePercentage = computed(() => {
|
||||
if (!memoryStats.value) return 0
|
||||
return Math.round((memoryStats.value.usedJSHeapSize / memoryStats.value.jsHeapSizeLimit) * 100)
|
||||
})
|
||||
|
||||
const maxTime = computed(() => {
|
||||
return Math.max(...resourceTimings.value.map(r => r.startTime + r.duration), 1)
|
||||
})
|
||||
|
||||
function getMetricClass(value, thresholds) {
|
||||
if (!value || !thresholds) return ''
|
||||
if (value <= thresholds.good) return 'good'
|
||||
if (value <= thresholds.needsImprovement) return 'needs-improvement'
|
||||
return 'poor'
|
||||
}
|
||||
|
||||
function getProgressColor(percentage) {
|
||||
if (percentage < 50) return '#67c23a'
|
||||
if (percentage < 80) return '#e6a23c'
|
||||
return '#f56c6c'
|
||||
}
|
||||
|
||||
function getResourceColor(type) {
|
||||
const colors = {
|
||||
script: '#409eff',
|
||||
stylesheet: '#67c23a',
|
||||
image: '#e6a23c',
|
||||
font: '#909399',
|
||||
fetch: '#f56c6c',
|
||||
xhr: '#f56c6c'
|
||||
}
|
||||
return colors[type] || '#909399'
|
||||
}
|
||||
|
||||
function formatTime(value, unit = 's') {
|
||||
if (!value) return 'N/A'
|
||||
if (unit === 'ms') {
|
||||
return `${Math.round(value)}ms`
|
||||
}
|
||||
return `${(value / 1000).toFixed(2)}s`
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
async function refreshMetrics() {
|
||||
// Get performance metrics
|
||||
const perfMetrics = performanceMonitor.getMetrics()
|
||||
metrics.value = perfMetrics
|
||||
|
||||
// Get memory stats
|
||||
if (performance.memory) {
|
||||
memoryStats.value = {
|
||||
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
||||
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
||||
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
|
||||
}
|
||||
}
|
||||
|
||||
// Get resource timings
|
||||
const resources = performance.getEntriesByType('resource')
|
||||
resourceTimings.value = resources
|
||||
.filter(r => r.duration > 0)
|
||||
.map(r => ({
|
||||
name: r.name.split('/').pop() || r.name,
|
||||
fullName: r.name,
|
||||
type: getResourceType(r),
|
||||
size: r.transferSize || 0,
|
||||
duration: Math.round(r.duration),
|
||||
startTime: Math.round(r.startTime)
|
||||
}))
|
||||
.sort((a, b) => b.duration - a.duration)
|
||||
.slice(0, 20) // Top 20 slowest resources
|
||||
}
|
||||
|
||||
function getResourceType(entry) {
|
||||
const url = entry.name
|
||||
if (url.match(/\.(js|mjs)$/)) return 'script'
|
||||
if (url.match(/\.css$/)) return 'stylesheet'
|
||||
if (url.match(/\.(png|jpg|jpeg|gif|svg|webp)$/)) return 'image'
|
||||
if (url.match(/\.(woff|woff2|ttf|eot|otf)$/)) return 'font'
|
||||
if (entry.initiatorType === 'fetch') return 'fetch'
|
||||
if (entry.initiatorType === 'xmlhttprequest') return 'xhr'
|
||||
return entry.initiatorType || 'other'
|
||||
}
|
||||
|
||||
async function clearCache() {
|
||||
try {
|
||||
await clearAllCaches()
|
||||
ElMessage.success('Cache cleared successfully')
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to clear cache')
|
||||
}
|
||||
}
|
||||
|
||||
async function runLighthouse() {
|
||||
ElMessage.info('Lighthouse analysis started...')
|
||||
// In real implementation, this would trigger a Lighthouse run
|
||||
}
|
||||
|
||||
function exportReport() {
|
||||
const report = {
|
||||
timestamp: new Date().toISOString(),
|
||||
metrics: metrics.value,
|
||||
memory: memoryStats.value,
|
||||
resources: resourceTimings.value
|
||||
}
|
||||
|
||||
const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `performance-report-${Date.now()}.json`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
ElMessage.success('Report exported successfully')
|
||||
}
|
||||
|
||||
let refreshInterval
|
||||
|
||||
onMounted(() => {
|
||||
refreshMetrics()
|
||||
// Auto-refresh every 30 seconds
|
||||
refreshInterval = setInterval(refreshMetrics, 30000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.performance-monitor {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.monitor-card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
padding: 16px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.metric-card.good {
|
||||
border-color: #67c23a;
|
||||
background-color: #f0f9ff;
|
||||
}
|
||||
|
||||
.metric-card.needs-improvement {
|
||||
border-color: #e6a23c;
|
||||
background-color: #fdf6ec;
|
||||
}
|
||||
|
||||
.metric-card.poor {
|
||||
border-color: #f56c6c;
|
||||
background-color: #fef0f0;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.metric-target {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin: 24px 0 16px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.memory-stats {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.resource-name {
|
||||
display: inline-block;
|
||||
max-width: 280px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.timeline-bar {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.actions .el-button {
|
||||
margin: 0 8px;
|
||||
}
|
||||
</style>
|
||||
144
marketing-agent/frontend/src/components/charts/FunnelChart.vue
Normal file
144
marketing-agent/frontend/src/components/charts/FunnelChart.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="funnel-chart">
|
||||
<div class="funnel-step" v-for="(step, index) in funnelSteps" :key="index">
|
||||
<div
|
||||
class="funnel-bar"
|
||||
:style="{
|
||||
width: `${step.percentage}%`,
|
||||
backgroundColor: getStepColor(index)
|
||||
}"
|
||||
>
|
||||
<div class="funnel-label">
|
||||
<span class="step-name">{{ step.name }}</span>
|
||||
<span class="step-value">{{ formatNumber(step.value) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="funnel-percentage">{{ step.percentage.toFixed(1) }}%</div>
|
||||
<div v-if="index < funnelSteps.length - 1" class="conversion-rate">
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
{{ step.conversionRate.toFixed(1) }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const funnelSteps = computed(() => {
|
||||
if (!props.data || props.data.length === 0) return []
|
||||
|
||||
const maxValue = Math.max(...props.data.map(item => item.value))
|
||||
|
||||
return props.data.map((item, index) => {
|
||||
const percentage = (item.value / maxValue) * 100
|
||||
const conversionRate = index > 0
|
||||
? (item.value / props.data[index - 1].value) * 100
|
||||
: 100
|
||||
|
||||
return {
|
||||
...item,
|
||||
percentage,
|
||||
conversionRate
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const getStepColor = (index) => {
|
||||
const colors = [
|
||||
'#409eff',
|
||||
'#67c23a',
|
||||
'#e6a23c',
|
||||
'#f56c6c',
|
||||
'#909399'
|
||||
]
|
||||
return colors[index % colors.length]
|
||||
}
|
||||
|
||||
const formatNumber = (value) => {
|
||||
if (value >= 1000000) {
|
||||
return `${(value / 1000000).toFixed(1)}M`
|
||||
} else if (value >= 1000) {
|
||||
return `${(value / 1000).toFixed(1)}K`
|
||||
}
|
||||
return value.toString()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.funnel-chart {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
|
||||
.funnel-step {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
|
||||
.funnel-bar {
|
||||
height: 50px;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.funnel-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
|
||||
.step-name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.step-value {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.funnel-percentage {
|
||||
position: absolute;
|
||||
right: -50px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.conversion-rate {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
207
marketing-agent/frontend/src/components/charts/HeatmapChart.vue
Normal file
207
marketing-agent/frontend/src/components/charts/HeatmapChart.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div class="heatmap-chart">
|
||||
<div class="heatmap-container">
|
||||
<div class="y-axis">
|
||||
<div v-for="day in days" :key="day" class="day-label">{{ day }}</div>
|
||||
</div>
|
||||
<div class="heatmap-grid">
|
||||
<div class="x-axis">
|
||||
<div v-for="hour in hours" :key="hour" class="hour-label">{{ hour }}</div>
|
||||
</div>
|
||||
<div class="cells">
|
||||
<div
|
||||
v-for="(cell, index) in heatmapCells"
|
||||
:key="index"
|
||||
class="heatmap-cell"
|
||||
:style="{
|
||||
backgroundColor: getCellColor(cell.value),
|
||||
opacity: getCellOpacity(cell.value)
|
||||
}"
|
||||
@mouseenter="showTooltip($event, cell)"
|
||||
@mouseleave="hideTooltip"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="heatmap-legend">
|
||||
<span class="legend-title">Activity Level:</span>
|
||||
<div class="legend-gradient"></div>
|
||||
<div class="legend-labels">
|
||||
<span>Low</span>
|
||||
<span>High</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-tooltip
|
||||
v-model:visible="tooltipVisible"
|
||||
:virtual-ref="tooltipRef"
|
||||
placement="top"
|
||||
:content="tooltipContent"
|
||||
:popper-options="{
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 8],
|
||||
},
|
||||
},
|
||||
],
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
||||
const hours = Array.from({ length: 24 }, (_, i) => i)
|
||||
|
||||
const tooltipVisible = ref(false)
|
||||
const tooltipRef = ref(null)
|
||||
const tooltipContent = ref('')
|
||||
|
||||
const heatmapCells = computed(() => {
|
||||
const cells = []
|
||||
for (let day = 0; day < 7; day++) {
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
const dataPoint = props.data.find(d => d.day === day && d.hour === hour)
|
||||
cells.push({
|
||||
day,
|
||||
hour,
|
||||
value: dataPoint?.value || 0
|
||||
})
|
||||
}
|
||||
}
|
||||
return cells
|
||||
})
|
||||
|
||||
const maxValue = computed(() => {
|
||||
return Math.max(...props.data.map(d => d.value), 1)
|
||||
})
|
||||
|
||||
const getCellColor = (value) => {
|
||||
// Use a blue color scheme
|
||||
return '#409eff'
|
||||
}
|
||||
|
||||
const getCellOpacity = (value) => {
|
||||
// Scale opacity based on value
|
||||
const normalized = value / maxValue.value
|
||||
return 0.1 + (normalized * 0.9)
|
||||
}
|
||||
|
||||
const showTooltip = (event, cell) => {
|
||||
tooltipRef.value = event.target
|
||||
tooltipContent.value = `${days[cell.day]} ${cell.hour}:00 - Activity: ${cell.value}`
|
||||
tooltipVisible.value = true
|
||||
}
|
||||
|
||||
const hideTooltip = () => {
|
||||
tooltipVisible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.heatmap-chart {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
|
||||
.heatmap-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.y-axis {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding-right: 10px;
|
||||
|
||||
.day-label {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.heatmap-grid {
|
||||
flex: 1;
|
||||
|
||||
.x-axis {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.hour-label {
|
||||
font-size: 11px;
|
||||
color: #606266;
|
||||
width: calc(100% / 24);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.cells {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(24, 1fr);
|
||||
grid-template-rows: repeat(7, 1fr);
|
||||
gap: 2px;
|
||||
background-color: #f5f7fa;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
|
||||
.heatmap-cell {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.2);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.heatmap-legend {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
|
||||
.legend-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.legend-gradient {
|
||||
width: 200px;
|
||||
height: 10px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgba(64, 158, 255, 0.1),
|
||||
rgba(64, 158, 255, 1)
|
||||
);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.legend-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 200px;
|
||||
margin-left: -210px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
98
marketing-agent/frontend/src/components/charts/LineChart.vue
Normal file
98
marketing-agent/frontend/src/components/charts/LineChart.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<canvas ref="chartCanvas"></canvas>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, onUnmounted } from 'vue'
|
||||
import {
|
||||
Chart,
|
||||
LineController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
LinearScale,
|
||||
CategoryScale,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
} from 'chart.js'
|
||||
|
||||
Chart.register(
|
||||
LineController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
LinearScale,
|
||||
CategoryScale,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
)
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 300
|
||||
}
|
||||
})
|
||||
|
||||
const chartCanvas = ref(null)
|
||||
let chartInstance = null
|
||||
|
||||
const createChart = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy()
|
||||
}
|
||||
|
||||
const ctx = chartCanvas.value.getContext('2d')
|
||||
chartInstance = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: props.data,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
...props.options
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateChart = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.data = props.data
|
||||
chartInstance.options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
...props.options
|
||||
}
|
||||
chartInstance.update('active')
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.data, updateChart, { deep: true })
|
||||
watch(() => props.options, updateChart, { deep: true })
|
||||
|
||||
onMounted(() => {
|
||||
chartCanvas.value.height = props.height
|
||||
createChart()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
canvas {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
98
marketing-agent/frontend/src/components/charts/Sparkline.vue
Normal file
98
marketing-agent/frontend/src/components/charts/Sparkline.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<svg
|
||||
:width="width"
|
||||
:height="height"
|
||||
class="sparkline"
|
||||
:viewBox="`0 0 ${width} ${height}`"
|
||||
>
|
||||
<path
|
||||
:d="sparklinePath"
|
||||
fill="none"
|
||||
:stroke="color"
|
||||
:stroke-width="strokeWidth"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle
|
||||
v-if="showDot && points.length > 0"
|
||||
:cx="points[points.length - 1].x"
|
||||
:cy="points[points.length - 1].y"
|
||||
:r="3"
|
||||
:fill="color"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 30
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#409eff'
|
||||
},
|
||||
strokeWidth: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
showDot: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
padding: {
|
||||
type: Number,
|
||||
default: 4
|
||||
}
|
||||
})
|
||||
|
||||
const points = computed(() => {
|
||||
if (!props.data || props.data.length === 0) return []
|
||||
|
||||
const values = props.data.filter(v => typeof v === 'number')
|
||||
if (values.length === 0) return []
|
||||
|
||||
const min = Math.min(...values)
|
||||
const max = Math.max(...values)
|
||||
const range = max - min || 1
|
||||
|
||||
const xStep = (props.width - props.padding * 2) / (values.length - 1 || 1)
|
||||
const yScale = (props.height - props.padding * 2) / range
|
||||
|
||||
return values.map((value, index) => ({
|
||||
x: props.padding + index * xStep,
|
||||
y: props.padding + (max - value) * yScale
|
||||
}))
|
||||
})
|
||||
|
||||
const sparklinePath = computed(() => {
|
||||
if (points.value.length === 0) return ''
|
||||
|
||||
const pathData = points.value.reduce((path, point, index) => {
|
||||
if (index === 0) {
|
||||
return `M ${point.x} ${point.y}`
|
||||
}
|
||||
return `${path} L ${point.x} ${point.y}`
|
||||
}, '')
|
||||
|
||||
return pathData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sparkline {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<div class="error-component">
|
||||
<div class="error-icon">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="error-title">{{ title }}</h3>
|
||||
<p class="error-message">{{ message }}</p>
|
||||
<el-button v-if="showRetry" type="primary" @click="$emit('retry')">
|
||||
Retry
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Oops! Something went wrong'
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: 'Failed to load the component. Please try again.'
|
||||
},
|
||||
showRetry: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['retry'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.error-component {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
color: #f56c6c;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin: 0 0 16px;
|
||||
max-width: 400px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="loading-component">
|
||||
<div class="loading-spinner">
|
||||
<div class="spinner-ring">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="message" class="loading-message">{{ message }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
message: {
|
||||
type: String,
|
||||
default: 'Loading...'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loading-component {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.spinner-ring {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.spinner-ring div {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 51px;
|
||||
height: 51px;
|
||||
margin: 6px;
|
||||
border: 6px solid #409eff;
|
||||
border-radius: 50%;
|
||||
animation: spinner-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
border-color: #409eff transparent transparent transparent;
|
||||
}
|
||||
|
||||
.spinner-ring div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
|
||||
.spinner-ring div:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
|
||||
.spinner-ring div:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
}
|
||||
|
||||
@keyframes spinner-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-message {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
165
marketing-agent/frontend/src/components/common/VirtualList.vue
Normal file
165
marketing-agent/frontend/src/components/common/VirtualList.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div ref="containerRef" class="virtual-list-container" @scroll="handleScroll">
|
||||
<div class="virtual-list-spacer" :style="{ height: totalHeight + 'px' }">
|
||||
<div
|
||||
class="virtual-list-content"
|
||||
:style="{ transform: `translateY(${offsetY}px)` }"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in visibleItems"
|
||||
:key="startIndex + index"
|
||||
class="virtual-list-item"
|
||||
:style="{ height: itemHeight + 'px' }"
|
||||
>
|
||||
<slot :item="item" :index="startIndex + index" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { throttle } from '@/utils/performance'
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
itemHeight: {
|
||||
type: Number,
|
||||
default: 50
|
||||
},
|
||||
buffer: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
throttleDelay: {
|
||||
type: Number,
|
||||
default: 16 // ~60fps
|
||||
}
|
||||
})
|
||||
|
||||
const containerRef = ref(null)
|
||||
const scrollTop = ref(0)
|
||||
const containerHeight = ref(0)
|
||||
|
||||
const totalHeight = computed(() => props.items.length * props.itemHeight)
|
||||
|
||||
const startIndex = computed(() => {
|
||||
return Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - props.buffer)
|
||||
})
|
||||
|
||||
const endIndex = computed(() => {
|
||||
return Math.min(
|
||||
props.items.length,
|
||||
Math.ceil((scrollTop.value + containerHeight.value) / props.itemHeight) + props.buffer
|
||||
)
|
||||
})
|
||||
|
||||
const visibleItems = computed(() => {
|
||||
return props.items.slice(startIndex.value, endIndex.value)
|
||||
})
|
||||
|
||||
const offsetY = computed(() => startIndex.value * props.itemHeight)
|
||||
|
||||
const handleScroll = throttle((event) => {
|
||||
scrollTop.value = event.target.scrollTop
|
||||
}, props.throttleDelay)
|
||||
|
||||
const updateContainerHeight = () => {
|
||||
if (containerRef.value) {
|
||||
containerHeight.value = containerRef.value.clientHeight
|
||||
}
|
||||
}
|
||||
|
||||
let resizeObserver = null
|
||||
|
||||
onMounted(() => {
|
||||
updateContainerHeight()
|
||||
|
||||
// Observe container resize
|
||||
if (window.ResizeObserver) {
|
||||
resizeObserver = new ResizeObserver(updateContainerHeight)
|
||||
resizeObserver.observe(containerRef.value)
|
||||
}
|
||||
|
||||
// Handle window resize as fallback
|
||||
window.addEventListener('resize', updateContainerHeight)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
window.removeEventListener('resize', updateContainerHeight)
|
||||
})
|
||||
|
||||
// Expose scroll methods
|
||||
defineExpose({
|
||||
scrollToIndex(index) {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.scrollTop = index * props.itemHeight
|
||||
}
|
||||
},
|
||||
scrollToTop() {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.scrollTop = 0
|
||||
}
|
||||
},
|
||||
scrollToBottom() {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.scrollTop = totalHeight.value
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.virtual-list-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.virtual-list-spacer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.virtual-list-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.virtual-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Custom scrollbar styles */
|
||||
.virtual-list-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.virtual-list-container::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.virtual-list-container::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.virtual-list-container::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="helper-reference">
|
||||
<el-collapse v-model="activeNames">
|
||||
<el-collapse-item
|
||||
v-for="category in helperCategories"
|
||||
:key="category.category"
|
||||
:title="category.category"
|
||||
:name="category.category"
|
||||
>
|
||||
<div v-for="helper in category.helpers" :key="helper.name" class="helper-item">
|
||||
<h4>{{ helper.name }}</h4>
|
||||
<div class="helper-syntax">
|
||||
<code>{{ helper.syntax }}</code>
|
||||
</div>
|
||||
<p class="helper-description">{{ helper.description }}</p>
|
||||
<div v-if="helper.examples" class="helper-examples">
|
||||
<h5>Examples:</h5>
|
||||
<div v-for="(example, index) in helper.examples" :key="index" class="example">
|
||||
<code>{{ example }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import api from '@/api'
|
||||
|
||||
const activeNames = ref([])
|
||||
const helperCategories = ref([])
|
||||
|
||||
const loadHelpers = async () => {
|
||||
try {
|
||||
const response = await api.get('/api/v1/template-variables/helpers')
|
||||
helperCategories.value = response.data.helpers
|
||||
activeNames.value = [helperCategories.value[0]?.category]
|
||||
} catch (error) {
|
||||
console.error('Failed to load helpers:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadHelpers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.helper-reference {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.helper-item {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.helper-item h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.helper-syntax {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.helper-description {
|
||||
margin: 10px 0;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.helper-examples {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.helper-examples h5 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.example {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #fff;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
color: #409eff;
|
||||
border: 1px solid #dcdfe6;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="variable-help">
|
||||
<el-collapse v-model="activeNames">
|
||||
<el-collapse-item
|
||||
v-for="category in variableCategories"
|
||||
:key="category.category"
|
||||
:title="category.category"
|
||||
:name="category.category"
|
||||
>
|
||||
<el-table :data="category.variables" stripe>
|
||||
<el-table-column prop="name" label="Variable" width="200">
|
||||
<template #default="{ row }">
|
||||
<code>{{ '{{' + row.name + '}}' }}</code>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="Type" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="Description" />
|
||||
</el-table>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import api from '@/api'
|
||||
|
||||
const activeNames = ref([])
|
||||
const variableCategories = ref([])
|
||||
|
||||
const loadVariables = async () => {
|
||||
try {
|
||||
const response = await api.get('/api/v1/template-variables/available')
|
||||
variableCategories.value = response.data.variables
|
||||
activeNames.value = [variableCategories.value[0]?.category]
|
||||
} catch (error) {
|
||||
console.error('Failed to load variables:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadVariables()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.variable-help {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f5f7fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
color: #409eff;
|
||||
}
|
||||
</style>
|
||||
171
marketing-agent/frontend/src/composables/useInfiniteScroll.js
Normal file
171
marketing-agent/frontend/src/composables/useInfiniteScroll.js
Normal file
@@ -0,0 +1,171 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { throttle } from '@/utils/performance'
|
||||
|
||||
/**
|
||||
* Infinite scroll composable for Vue 3
|
||||
* Provides efficient infinite scrolling with performance optimizations
|
||||
*/
|
||||
export function useInfiniteScroll(options = {}) {
|
||||
const {
|
||||
threshold = 100,
|
||||
throttleDelay = 200,
|
||||
onLoadMore,
|
||||
enabled = true
|
||||
} = options
|
||||
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const error = ref(null)
|
||||
const containerRef = ref(null)
|
||||
|
||||
let scrollHandler = null
|
||||
|
||||
const checkScroll = async () => {
|
||||
if (!enabled || loading.value || finished.value || !containerRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const container = containerRef.value
|
||||
const scrollHeight = container.scrollHeight
|
||||
const scrollTop = container.scrollTop
|
||||
const clientHeight = container.clientHeight
|
||||
|
||||
if (scrollHeight - scrollTop - clientHeight <= threshold) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const hasMore = await onLoadMore()
|
||||
finished.value = !hasMore
|
||||
} catch (err) {
|
||||
error.value = err
|
||||
console.error('Error loading more items:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
loading.value = false
|
||||
finished.value = false
|
||||
error.value = null
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (containerRef.value) {
|
||||
scrollHandler = throttle(checkScroll, throttleDelay)
|
||||
containerRef.value.addEventListener('scroll', scrollHandler, { passive: true })
|
||||
|
||||
// Check initial state
|
||||
checkScroll()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (containerRef.value && scrollHandler) {
|
||||
containerRef.value.removeEventListener('scroll', scrollHandler)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
loading,
|
||||
finished,
|
||||
error,
|
||||
reset,
|
||||
checkScroll
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual infinite scroll composable
|
||||
* Combines virtual scrolling with infinite scroll for maximum performance
|
||||
*/
|
||||
export function useVirtualInfiniteScroll(options = {}) {
|
||||
const {
|
||||
itemHeight = 50,
|
||||
buffer = 5,
|
||||
threshold = 100,
|
||||
throttleDelay = 200,
|
||||
onLoadMore,
|
||||
items = ref([])
|
||||
} = options
|
||||
|
||||
const containerRef = ref(null)
|
||||
const scrollTop = ref(0)
|
||||
const containerHeight = ref(0)
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
|
||||
// Calculate visible items
|
||||
const visibleItems = computed(() => {
|
||||
const startIndex = Math.floor(scrollTop.value / itemHeight)
|
||||
const endIndex = Math.ceil((scrollTop.value + containerHeight.value) / itemHeight)
|
||||
|
||||
const start = Math.max(0, startIndex - buffer)
|
||||
const end = Math.min(items.value.length, endIndex + buffer)
|
||||
|
||||
return {
|
||||
items: items.value.slice(start, end),
|
||||
startIndex: start,
|
||||
endIndex: end,
|
||||
offsetY: start * itemHeight
|
||||
}
|
||||
})
|
||||
|
||||
const totalHeight = computed(() => items.value.length * itemHeight)
|
||||
|
||||
const handleScroll = throttle(async (event) => {
|
||||
const container = event.target
|
||||
scrollTop.value = container.scrollTop
|
||||
|
||||
// Check if need to load more
|
||||
const scrollHeight = container.scrollHeight
|
||||
const clientHeight = container.clientHeight
|
||||
|
||||
if (scrollHeight - scrollTop.value - clientHeight <= threshold && !loading.value && !finished.value) {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const hasMore = await onLoadMore()
|
||||
finished.value = !hasMore
|
||||
} catch (error) {
|
||||
console.error('Error loading more items:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}, throttleDelay)
|
||||
|
||||
const updateContainerHeight = () => {
|
||||
if (containerRef.value) {
|
||||
containerHeight.value = containerRef.value.clientHeight
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (containerRef.value) {
|
||||
updateContainerHeight()
|
||||
containerRef.value.addEventListener('scroll', handleScroll, { passive: true })
|
||||
|
||||
// Update container height on resize
|
||||
const resizeObserver = new ResizeObserver(updateContainerHeight)
|
||||
resizeObserver.observe(containerRef.value)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.removeEventListener('scroll', handleScroll)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
visibleItems,
|
||||
totalHeight,
|
||||
loading,
|
||||
finished
|
||||
}
|
||||
}
|
||||
43
marketing-agent/frontend/src/composables/useResponsive.js
Normal file
43
marketing-agent/frontend/src/composables/useResponsive.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
export function useResponsive() {
|
||||
const isMobile = ref(false)
|
||||
const isTablet = ref(false)
|
||||
const isDesktop = ref(false)
|
||||
const screenWidth = ref(window.innerWidth)
|
||||
const screenHeight = ref(window.innerHeight)
|
||||
|
||||
// Breakpoints
|
||||
const breakpoints = {
|
||||
mobile: 768,
|
||||
tablet: 1024,
|
||||
desktop: 1280
|
||||
}
|
||||
|
||||
const updateDeviceType = () => {
|
||||
screenWidth.value = window.innerWidth
|
||||
screenHeight.value = window.innerHeight
|
||||
|
||||
isMobile.value = screenWidth.value < breakpoints.mobile
|
||||
isTablet.value = screenWidth.value >= breakpoints.mobile && screenWidth.value < breakpoints.desktop
|
||||
isDesktop.value = screenWidth.value >= breakpoints.desktop
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateDeviceType()
|
||||
window.addEventListener('resize', updateDeviceType)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateDeviceType)
|
||||
})
|
||||
|
||||
return {
|
||||
isMobile,
|
||||
isTablet,
|
||||
isDesktop,
|
||||
screenWidth,
|
||||
screenHeight,
|
||||
breakpoints
|
||||
}
|
||||
}
|
||||
191
marketing-agent/frontend/src/composables/useWebWorker.js
Normal file
191
marketing-agent/frontend/src/composables/useWebWorker.js
Normal file
@@ -0,0 +1,191 @@
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
|
||||
/**
|
||||
* Web Worker composable for offloading heavy computations
|
||||
*/
|
||||
export function useWebWorker(workerScript) {
|
||||
const worker = ref(null)
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
const result = ref(null)
|
||||
|
||||
// Create worker
|
||||
const createWorker = () => {
|
||||
if (typeof Worker !== 'undefined') {
|
||||
worker.value = new Worker(workerScript)
|
||||
|
||||
worker.value.onmessage = (event) => {
|
||||
loading.value = false
|
||||
result.value = event.data
|
||||
}
|
||||
|
||||
worker.value.onerror = (err) => {
|
||||
loading.value = false
|
||||
error.value = err
|
||||
console.error('Worker error:', err)
|
||||
}
|
||||
} else {
|
||||
error.value = new Error('Web Workers not supported')
|
||||
}
|
||||
}
|
||||
|
||||
// Send message to worker
|
||||
const postMessage = (data) => {
|
||||
if (!worker.value) {
|
||||
createWorker()
|
||||
}
|
||||
|
||||
if (worker.value) {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
worker.value.postMessage(data)
|
||||
}
|
||||
}
|
||||
|
||||
// Terminate worker
|
||||
const terminate = () => {
|
||||
if (worker.value) {
|
||||
worker.value.terminate()
|
||||
worker.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up on unmount
|
||||
onUnmounted(() => {
|
||||
terminate()
|
||||
})
|
||||
|
||||
return {
|
||||
postMessage,
|
||||
terminate,
|
||||
loading,
|
||||
error,
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared Worker composable for cross-tab communication
|
||||
*/
|
||||
export function useSharedWorker(workerScript, name) {
|
||||
const worker = ref(null)
|
||||
const port = ref(null)
|
||||
const connected = ref(false)
|
||||
const messages = ref([])
|
||||
|
||||
const connect = () => {
|
||||
if (typeof SharedWorker !== 'undefined') {
|
||||
worker.value = new SharedWorker(workerScript, name)
|
||||
port.value = worker.value.port
|
||||
|
||||
port.value.onmessage = (event) => {
|
||||
messages.value.push(event.data)
|
||||
}
|
||||
|
||||
port.value.start()
|
||||
connected.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const sendMessage = (data) => {
|
||||
if (port.value && connected.value) {
|
||||
port.value.postMessage(data)
|
||||
}
|
||||
}
|
||||
|
||||
const disconnect = () => {
|
||||
if (port.value) {
|
||||
port.value.close()
|
||||
port.value = null
|
||||
connected.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnect()
|
||||
})
|
||||
|
||||
return {
|
||||
connect,
|
||||
sendMessage,
|
||||
disconnect,
|
||||
connected,
|
||||
messages
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create inline worker from function
|
||||
*/
|
||||
export function createInlineWorker(fn) {
|
||||
const blob = new Blob([`(${fn.toString()})()`], { type: 'application/javascript' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const worker = new Worker(url)
|
||||
|
||||
// Clean up blob URL after worker is created
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
return worker
|
||||
}
|
||||
|
||||
/**
|
||||
* Heavy computation worker utility
|
||||
*/
|
||||
export function useComputationWorker() {
|
||||
const workerCode = `
|
||||
self.onmessage = function(e) {
|
||||
const { type, data } = e.data
|
||||
|
||||
switch(type) {
|
||||
case 'sort':
|
||||
const sorted = data.sort((a, b) => a - b)
|
||||
self.postMessage({ type: 'sort', result: sorted })
|
||||
break
|
||||
|
||||
case 'filter':
|
||||
const filtered = data.items.filter(item => item[data.key] === data.value)
|
||||
self.postMessage({ type: 'filter', result: filtered })
|
||||
break
|
||||
|
||||
case 'aggregate':
|
||||
const aggregated = data.reduce((acc, item) => {
|
||||
acc[item.category] = (acc[item.category] || 0) + item.value
|
||||
return acc
|
||||
}, {})
|
||||
self.postMessage({ type: 'aggregate', result: aggregated })
|
||||
break
|
||||
|
||||
case 'search':
|
||||
const searchResults = data.items.filter(item =>
|
||||
item.toLowerCase().includes(data.query.toLowerCase())
|
||||
)
|
||||
self.postMessage({ type: 'search', result: searchResults })
|
||||
break
|
||||
|
||||
default:
|
||||
self.postMessage({ type: 'error', error: 'Unknown operation' })
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const blob = new Blob([workerCode], { type: 'application/javascript' })
|
||||
const workerUrl = URL.createObjectURL(blob)
|
||||
|
||||
const { postMessage, terminate, loading, error, result } = useWebWorker(workerUrl)
|
||||
|
||||
// Clean up blob URL
|
||||
onUnmounted(() => {
|
||||
URL.revokeObjectURL(workerUrl)
|
||||
})
|
||||
|
||||
return {
|
||||
sort: (data) => postMessage({ type: 'sort', data }),
|
||||
filter: (items, key, value) => postMessage({ type: 'filter', data: { items, key, value } }),
|
||||
aggregate: (data) => postMessage({ type: 'aggregate', data }),
|
||||
search: (items, query) => postMessage({ type: 'search', data: { items, query } }),
|
||||
terminate,
|
||||
loading,
|
||||
error,
|
||||
result
|
||||
}
|
||||
}
|
||||
154
marketing-agent/frontend/src/locales/en.json
Normal file
154
marketing-agent/frontend/src/locales/en.json
Normal file
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"common": {
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"create": "Create",
|
||||
"search": "Search",
|
||||
"filter": "Filter",
|
||||
"export": "Export",
|
||||
"import": "Import",
|
||||
"refresh": "Refresh",
|
||||
"loading": "Loading...",
|
||||
"success": "Success",
|
||||
"error": "Error",
|
||||
"warning": "Warning",
|
||||
"info": "Info",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"all": "All",
|
||||
"none": "None",
|
||||
"status": "Status",
|
||||
"actions": "Actions",
|
||||
"startTime": "Start Time",
|
||||
"endTime": "End Time",
|
||||
"createdAt": "Created At",
|
||||
"updatedAt": "Updated At"
|
||||
},
|
||||
"menu": {
|
||||
"dashboard": "Dashboard",
|
||||
"campaigns": "Campaigns",
|
||||
"analytics": "Analytics",
|
||||
"abTesting": "A/B Testing",
|
||||
"accounts": "Accounts",
|
||||
"settings": "Settings",
|
||||
"compliance": "Compliance"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"register": "Register",
|
||||
"username": "Username",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"confirmPassword": "Confirm Password",
|
||||
"rememberMe": "Remember Me",
|
||||
"forgotPassword": "Forgot Password?",
|
||||
"loginSuccess": "Login successful",
|
||||
"logoutSuccess": "Logout successful",
|
||||
"registerSuccess": "Registration successful"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"overview": "Overview",
|
||||
"activeCampaigns": "Active Campaigns",
|
||||
"totalMessages": "Total Messages",
|
||||
"engagementRate": "Engagement Rate",
|
||||
"conversionRate": "Conversion Rate",
|
||||
"recentActivity": "Recent Activity",
|
||||
"quickActions": "Quick Actions",
|
||||
"createCampaign": "Create Campaign",
|
||||
"viewAnalytics": "View Analytics",
|
||||
"manageAccounts": "Manage Accounts"
|
||||
},
|
||||
"campaigns": {
|
||||
"title": "Campaigns",
|
||||
"list": "Campaign List",
|
||||
"create": "Create Campaign",
|
||||
"edit": "Edit Campaign",
|
||||
"detail": "Campaign Detail",
|
||||
"name": "Campaign Name",
|
||||
"description": "Description",
|
||||
"targetAudience": "Target Audience",
|
||||
"messages": "Messages",
|
||||
"schedule": "Schedule",
|
||||
"goals": "Goals",
|
||||
"status": {
|
||||
"draft": "Draft",
|
||||
"active": "Active",
|
||||
"paused": "Paused",
|
||||
"completed": "Completed",
|
||||
"cancelled": "Cancelled"
|
||||
},
|
||||
"actions": {
|
||||
"start": "Start",
|
||||
"pause": "Pause",
|
||||
"resume": "Resume",
|
||||
"cancel": "Cancel",
|
||||
"duplicate": "Duplicate"
|
||||
}
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
"overview": "Analytics Overview",
|
||||
"metrics": "Metrics",
|
||||
"impressions": "Impressions",
|
||||
"clicks": "Clicks",
|
||||
"conversions": "Conversions",
|
||||
"engagement": "Engagement",
|
||||
"timeRange": "Time Range",
|
||||
"today": "Today",
|
||||
"yesterday": "Yesterday",
|
||||
"last7Days": "Last 7 Days",
|
||||
"last30Days": "Last 30 Days",
|
||||
"custom": "Custom Range"
|
||||
},
|
||||
"abTesting": {
|
||||
"title": "A/B Testing",
|
||||
"experiments": "Experiments",
|
||||
"createExperiment": "Create Experiment",
|
||||
"variants": "Variants",
|
||||
"control": "Control",
|
||||
"treatment": "Treatment",
|
||||
"allocation": "Traffic Allocation",
|
||||
"significance": "Statistical Significance",
|
||||
"winner": "Winner",
|
||||
"results": "Results"
|
||||
},
|
||||
"accounts": {
|
||||
"title": "Accounts",
|
||||
"telegram": "Telegram Accounts",
|
||||
"add": "Add Account",
|
||||
"phoneNumber": "Phone Number",
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"groups": "Groups",
|
||||
"syncGroups": "Sync Groups",
|
||||
"lastSync": "Last Sync"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"profile": "Profile",
|
||||
"apiKeys": "API Keys",
|
||||
"notifications": "Notifications",
|
||||
"language": "Language",
|
||||
"theme": "Theme",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
},
|
||||
"compliance": {
|
||||
"title": "Compliance",
|
||||
"consent": "Consent Management",
|
||||
"privacy": "Privacy Rights",
|
||||
"dataExport": "Data Export",
|
||||
"dataDeletion": "Data Deletion",
|
||||
"auditLogs": "Audit Logs",
|
||||
"gdpr": "GDPR Compliance",
|
||||
"ccpa": "CCPA Compliance",
|
||||
"compliant": "Compliant",
|
||||
"nonCompliant": "Non-Compliant"
|
||||
}
|
||||
}
|
||||
17
marketing-agent/frontend/src/locales/index.js
Normal file
17
marketing-agent/frontend/src/locales/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import en from './en.json'
|
||||
import zh from './zh.json'
|
||||
|
||||
const messages = {
|
||||
en,
|
||||
zh
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: localStorage.getItem('locale') || 'en',
|
||||
fallbackLocale: 'en',
|
||||
messages
|
||||
})
|
||||
|
||||
export default i18n
|
||||
154
marketing-agent/frontend/src/locales/zh.json
Normal file
154
marketing-agent/frontend/src/locales/zh.json
Normal file
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"common": {
|
||||
"confirm": "确认",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"create": "创建",
|
||||
"search": "搜索",
|
||||
"filter": "筛选",
|
||||
"export": "导出",
|
||||
"import": "导入",
|
||||
"refresh": "刷新",
|
||||
"loading": "加载中...",
|
||||
"success": "成功",
|
||||
"error": "错误",
|
||||
"warning": "警告",
|
||||
"info": "信息",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"all": "全部",
|
||||
"none": "无",
|
||||
"status": "状态",
|
||||
"actions": "操作",
|
||||
"startTime": "开始时间",
|
||||
"endTime": "结束时间",
|
||||
"createdAt": "创建时间",
|
||||
"updatedAt": "更新时间"
|
||||
},
|
||||
"menu": {
|
||||
"dashboard": "仪表盘",
|
||||
"campaigns": "营销活动",
|
||||
"analytics": "数据分析",
|
||||
"abTesting": "A/B 测试",
|
||||
"accounts": "账号管理",
|
||||
"settings": "设置",
|
||||
"compliance": "合规管理"
|
||||
},
|
||||
"auth": {
|
||||
"login": "登录",
|
||||
"logout": "退出",
|
||||
"register": "注册",
|
||||
"username": "用户名",
|
||||
"email": "邮箱",
|
||||
"password": "密码",
|
||||
"confirmPassword": "确认密码",
|
||||
"rememberMe": "记住我",
|
||||
"forgotPassword": "忘记密码?",
|
||||
"loginSuccess": "登录成功",
|
||||
"logoutSuccess": "退出成功",
|
||||
"registerSuccess": "注册成功"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "仪表盘",
|
||||
"overview": "概览",
|
||||
"activeCampaigns": "活跃营销活动",
|
||||
"totalMessages": "消息总数",
|
||||
"engagementRate": "互动率",
|
||||
"conversionRate": "转化率",
|
||||
"recentActivity": "最近活动",
|
||||
"quickActions": "快捷操作",
|
||||
"createCampaign": "创建营销活动",
|
||||
"viewAnalytics": "查看分析",
|
||||
"manageAccounts": "管理账号"
|
||||
},
|
||||
"campaigns": {
|
||||
"title": "营销活动",
|
||||
"list": "活动列表",
|
||||
"create": "创建活动",
|
||||
"edit": "编辑活动",
|
||||
"detail": "活动详情",
|
||||
"name": "活动名称",
|
||||
"description": "描述",
|
||||
"targetAudience": "目标受众",
|
||||
"messages": "消息",
|
||||
"schedule": "计划",
|
||||
"goals": "目标",
|
||||
"status": {
|
||||
"draft": "草稿",
|
||||
"active": "进行中",
|
||||
"paused": "已暂停",
|
||||
"completed": "已完成",
|
||||
"cancelled": "已取消"
|
||||
},
|
||||
"actions": {
|
||||
"start": "开始",
|
||||
"pause": "暂停",
|
||||
"resume": "恢复",
|
||||
"cancel": "取消",
|
||||
"duplicate": "复制"
|
||||
}
|
||||
},
|
||||
"analytics": {
|
||||
"title": "数据分析",
|
||||
"overview": "分析概览",
|
||||
"metrics": "指标",
|
||||
"impressions": "展示次数",
|
||||
"clicks": "点击次数",
|
||||
"conversions": "转化次数",
|
||||
"engagement": "互动率",
|
||||
"timeRange": "时间范围",
|
||||
"today": "今天",
|
||||
"yesterday": "昨天",
|
||||
"last7Days": "最近7天",
|
||||
"last30Days": "最近30天",
|
||||
"custom": "自定义范围"
|
||||
},
|
||||
"abTesting": {
|
||||
"title": "A/B 测试",
|
||||
"experiments": "实验",
|
||||
"createExperiment": "创建实验",
|
||||
"variants": "变体",
|
||||
"control": "对照组",
|
||||
"treatment": "实验组",
|
||||
"allocation": "流量分配",
|
||||
"significance": "统计显著性",
|
||||
"winner": "获胜者",
|
||||
"results": "结果"
|
||||
},
|
||||
"accounts": {
|
||||
"title": "账号管理",
|
||||
"telegram": "Telegram 账号",
|
||||
"add": "添加账号",
|
||||
"phoneNumber": "手机号码",
|
||||
"active": "活跃",
|
||||
"inactive": "未激活",
|
||||
"groups": "群组",
|
||||
"syncGroups": "同步群组",
|
||||
"lastSync": "最后同步"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
"profile": "个人资料",
|
||||
"apiKeys": "API 密钥",
|
||||
"notifications": "通知",
|
||||
"language": "语言",
|
||||
"theme": "主题",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"system": "跟随系统"
|
||||
},
|
||||
"compliance": {
|
||||
"title": "合规管理",
|
||||
"consent": "同意管理",
|
||||
"privacy": "隐私权",
|
||||
"dataExport": "数据导出",
|
||||
"dataDeletion": "数据删除",
|
||||
"auditLogs": "审计日志",
|
||||
"gdpr": "GDPR 合规",
|
||||
"ccpa": "CCPA 合规",
|
||||
"compliant": "合规",
|
||||
"nonCompliant": "不合规"
|
||||
}
|
||||
}
|
||||
26
marketing-agent/frontend/src/main.js
Normal file
26
marketing-agent/frontend/src/main.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import './style.css'
|
||||
import './styles/mobile.css'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import i18n from './locales'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
// Register all Element Plus icons
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
app.use(i18n)
|
||||
|
||||
app.mount('#app')
|
||||
128
marketing-agent/frontend/src/main.optimized.js
Normal file
128
marketing-agent/frontend/src/main.optimized.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import './style.css'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import i18n from './locales'
|
||||
|
||||
// Performance utilities
|
||||
import { performanceMonitor, setupImageLazyLoading, requestIdleCallback } from './utils/performance'
|
||||
import { registerServiceWorker, setupNetworkListeners, requestNotificationPermission } from './utils/serviceWorker'
|
||||
import { registerLazyLoadDirectives } from './plugins/lazyload'
|
||||
import { createPersistedState } from './stores/plugins/persistedState'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
// Add persisted state plugin to Pinia
|
||||
pinia.use(createPersistedState({
|
||||
paths: ['user', 'settings', 'cache'],
|
||||
debounceTime: 1000
|
||||
}))
|
||||
|
||||
// Register Element Plus icons on-demand
|
||||
const registerIcons = () => {
|
||||
const icons = ['User', 'Lock', 'Message', 'Search', 'Plus', 'Delete', 'Edit', 'View', 'Download', 'Upload']
|
||||
icons.forEach(name => {
|
||||
if (ElementPlusIconsVue[name]) {
|
||||
app.component(name, ElementPlusIconsVue[name])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Register all icons in idle time for better performance
|
||||
requestIdleCallback(() => {
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
if (!app.component(key)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Register initial required icons
|
||||
registerIcons()
|
||||
|
||||
// Register lazy loading directives
|
||||
registerLazyLoadDirectives(app)
|
||||
|
||||
// Global error handler
|
||||
app.config.errorHandler = (err, instance, info) => {
|
||||
console.error('Global error:', err, info)
|
||||
// Send error to monitoring service
|
||||
performanceMonitor.reportError({
|
||||
error: err.toString(),
|
||||
componentInfo: info,
|
||||
stack: err.stack,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
// Global performance config
|
||||
app.config.performance = true
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(ElementPlus, {
|
||||
// Element Plus performance options
|
||||
size: 'default',
|
||||
zIndex: 3000
|
||||
})
|
||||
app.use(i18n)
|
||||
|
||||
// Mount app
|
||||
app.mount('#app')
|
||||
|
||||
// Post-mount optimizations
|
||||
requestIdleCallback(async () => {
|
||||
// Initialize performance monitoring
|
||||
performanceMonitor.monitorLongTasks()
|
||||
|
||||
// Report initial metrics after 5 seconds
|
||||
setTimeout(() => {
|
||||
performanceMonitor.reportMetrics()
|
||||
}, 5000)
|
||||
|
||||
// Setup image lazy loading
|
||||
setupImageLazyLoading()
|
||||
|
||||
// Register service worker
|
||||
if (import.meta.env.PROD) {
|
||||
const registration = await registerServiceWorker()
|
||||
|
||||
// Request notification permission
|
||||
if (registration) {
|
||||
await requestNotificationPermission()
|
||||
}
|
||||
}
|
||||
|
||||
// Setup network listeners
|
||||
setupNetworkListeners({
|
||||
onOnline: () => {
|
||||
// Sync data when back online
|
||||
window.dispatchEvent(new CustomEvent('app:online'))
|
||||
},
|
||||
onOffline: () => {
|
||||
// Show offline notification
|
||||
window.dispatchEvent(new CustomEvent('app:offline'))
|
||||
}
|
||||
})
|
||||
|
||||
// Preload critical routes
|
||||
const criticalRoutes = ['/dashboard', '/campaigns', '/users']
|
||||
criticalRoutes.forEach(route => {
|
||||
router.resolve(route)
|
||||
})
|
||||
})
|
||||
|
||||
// Development helpers
|
||||
if (import.meta.env.DEV) {
|
||||
// Performance debugging
|
||||
window.__PERFORMANCE__ = performanceMonitor
|
||||
|
||||
// Enable Vue devtools
|
||||
app.config.devtools = true
|
||||
}
|
||||
236
marketing-agent/frontend/src/plugins/lazyload.js
Normal file
236
marketing-agent/frontend/src/plugins/lazyload.js
Normal file
@@ -0,0 +1,236 @@
|
||||
// Vue 3 lazy loading directive for images and components
|
||||
|
||||
/**
|
||||
* Lazy load directive for images
|
||||
*/
|
||||
export const LazyLoadDirective = {
|
||||
mounted(el, binding) {
|
||||
const options = {
|
||||
root: null,
|
||||
rootMargin: '0px',
|
||||
threshold: 0.1
|
||||
}
|
||||
|
||||
const imageObserver = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target
|
||||
const src = binding.value
|
||||
|
||||
// Create a new image to preload
|
||||
const tempImg = new Image()
|
||||
|
||||
tempImg.onload = () => {
|
||||
// Add fade-in animation
|
||||
img.style.opacity = '0'
|
||||
img.src = src
|
||||
img.style.transition = 'opacity 0.3s'
|
||||
|
||||
// Force reflow
|
||||
img.offsetHeight
|
||||
|
||||
img.style.opacity = '1'
|
||||
img.classList.add('lazy-loaded')
|
||||
|
||||
// Emit custom event
|
||||
img.dispatchEvent(new CustomEvent('lazyloaded'))
|
||||
}
|
||||
|
||||
tempImg.onerror = () => {
|
||||
// Use placeholder on error
|
||||
img.src = binding.arg || '/images/placeholder.png'
|
||||
img.classList.add('lazy-error')
|
||||
|
||||
// Emit error event
|
||||
img.dispatchEvent(new CustomEvent('lazyerror'))
|
||||
}
|
||||
|
||||
tempImg.src = src
|
||||
observer.unobserve(img)
|
||||
}
|
||||
})
|
||||
}, options)
|
||||
|
||||
// Start observing
|
||||
imageObserver.observe(el)
|
||||
|
||||
// Store observer for cleanup
|
||||
el._imageObserver = imageObserver
|
||||
},
|
||||
|
||||
unmounted(el) {
|
||||
if (el._imageObserver) {
|
||||
el._imageObserver.disconnect()
|
||||
delete el._imageObserver
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy load directive for background images
|
||||
*/
|
||||
export const LazyBackgroundDirective = {
|
||||
mounted(el, binding) {
|
||||
const options = {
|
||||
root: null,
|
||||
rootMargin: '50px',
|
||||
threshold: 0.01
|
||||
}
|
||||
|
||||
const bgObserver = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const element = entry.target
|
||||
const src = binding.value
|
||||
|
||||
// Preload image
|
||||
const tempImg = new Image()
|
||||
|
||||
tempImg.onload = () => {
|
||||
element.style.backgroundImage = `url(${src})`
|
||||
element.classList.add('lazy-bg-loaded')
|
||||
}
|
||||
|
||||
tempImg.src = src
|
||||
observer.unobserve(element)
|
||||
}
|
||||
})
|
||||
}, options)
|
||||
|
||||
// Add loading class
|
||||
el.classList.add('lazy-bg-loading')
|
||||
|
||||
// Start observing
|
||||
bgObserver.observe(el)
|
||||
|
||||
// Store observer for cleanup
|
||||
el._bgObserver = bgObserver
|
||||
},
|
||||
|
||||
unmounted(el) {
|
||||
if (el._bgObserver) {
|
||||
el._bgObserver.disconnect()
|
||||
delete el._bgObserver
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Progressive image loading
|
||||
*/
|
||||
export const ProgressiveImageDirective = {
|
||||
mounted(el, binding) {
|
||||
const { lowQuality, highQuality } = binding.value
|
||||
|
||||
// Load low quality first
|
||||
el.src = lowQuality
|
||||
el.classList.add('progressive-loading')
|
||||
|
||||
// Create intersection observer for high quality
|
||||
const options = {
|
||||
root: null,
|
||||
rootMargin: '50px',
|
||||
threshold: 0.01
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target
|
||||
const highQualityImg = new Image()
|
||||
|
||||
highQualityImg.onload = () => {
|
||||
img.src = highQuality
|
||||
img.classList.remove('progressive-loading')
|
||||
img.classList.add('progressive-loaded')
|
||||
}
|
||||
|
||||
highQualityImg.src = highQuality
|
||||
observer.unobserve(img)
|
||||
}
|
||||
})
|
||||
}, options)
|
||||
|
||||
observer.observe(el)
|
||||
el._progressiveObserver = observer
|
||||
},
|
||||
|
||||
unmounted(el) {
|
||||
if (el._progressiveObserver) {
|
||||
el._progressiveObserver.disconnect()
|
||||
delete el._progressiveObserver
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy component loader
|
||||
*/
|
||||
export function createLazyComponent(loader, options = {}) {
|
||||
const {
|
||||
loadingComponent = null,
|
||||
errorComponent = null,
|
||||
delay = 200,
|
||||
timeout = 10000,
|
||||
suspensible = false
|
||||
} = options
|
||||
|
||||
return {
|
||||
loader,
|
||||
loadingComponent,
|
||||
errorComponent,
|
||||
delay,
|
||||
timeout,
|
||||
suspensible,
|
||||
onError(error, retry, fail, attempts) {
|
||||
if (attempts <= 3) {
|
||||
// Retry up to 3 times
|
||||
setTimeout(retry, 1000 * attempts)
|
||||
} else {
|
||||
console.error('Failed to load component after 3 attempts:', error)
|
||||
fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all lazy loading directives
|
||||
*/
|
||||
export function registerLazyLoadDirectives(app) {
|
||||
app.directive('lazy', LazyLoadDirective)
|
||||
app.directive('lazy-bg', LazyBackgroundDirective)
|
||||
app.directive('progressive', ProgressiveImageDirective)
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload images utility
|
||||
*/
|
||||
export function preloadImages(urls) {
|
||||
const promises = urls.map(url => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.onload = () => resolve(url)
|
||||
img.onerror = () => reject(new Error(`Failed to load image: ${url}`))
|
||||
img.src = url
|
||||
})
|
||||
})
|
||||
|
||||
return Promise.allSettled(promises)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create responsive image set
|
||||
*/
|
||||
export function createResponsiveImageSet(baseUrl, sizes = [320, 640, 1024, 1920]) {
|
||||
const srcSet = sizes.map(size => {
|
||||
const url = baseUrl.replace(/(\.[^.]+)$/, `-${size}w$1`)
|
||||
return `${url} ${size}w`
|
||||
}).join(', ')
|
||||
|
||||
return {
|
||||
src: baseUrl,
|
||||
srcSet,
|
||||
sizes: '(max-width: 320px) 320px, (max-width: 640px) 640px, (max-width: 1024px) 1024px, 1920px'
|
||||
}
|
||||
}
|
||||
197
marketing-agent/frontend/src/router/index.js
Normal file
197
marketing-agent/frontend/src/router/index.js
Normal file
@@ -0,0 +1,197 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/Login.vue'),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/login-debug',
|
||||
name: 'LoginDebug',
|
||||
component: () => import('@/views/LoginDebug.vue'),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/test',
|
||||
name: 'TestSimple',
|
||||
component: () => import('@/views/TestSimple.vue'),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('@/views/HomeTest.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
component: () => import('@/views/LayoutSimple.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'Dashboard',
|
||||
component: () => import('@/views/DashboardEnhanced.vue')
|
||||
},
|
||||
{
|
||||
path: 'campaigns',
|
||||
name: 'Campaigns',
|
||||
component: () => import('@/views/campaigns/CampaignList.vue')
|
||||
},
|
||||
{
|
||||
path: 'campaigns/create',
|
||||
name: 'CreateCampaign',
|
||||
component: () => import('@/views/campaigns/CreateCampaign.vue')
|
||||
},
|
||||
{
|
||||
path: 'campaigns/:id',
|
||||
name: 'CampaignDetail',
|
||||
component: () => import('@/views/campaigns/CampaignDetail.vue')
|
||||
},
|
||||
{
|
||||
path: 'analytics',
|
||||
name: 'Analytics',
|
||||
component: () => import('@/views/Analytics.vue')
|
||||
},
|
||||
{
|
||||
path: 'ab-testing',
|
||||
name: 'ABTesting',
|
||||
component: () => import('@/views/ABTesting.vue')
|
||||
},
|
||||
{
|
||||
path: 'accounts',
|
||||
name: 'Accounts',
|
||||
component: () => import('@/views/AccountsEnhanced.vue')
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'Settings',
|
||||
component: () => import('@/views/Settings.vue')
|
||||
},
|
||||
{
|
||||
path: 'compliance',
|
||||
name: 'Compliance',
|
||||
component: () => import('@/views/Compliance.vue')
|
||||
},
|
||||
{
|
||||
path: 'data-exchange',
|
||||
name: 'DataExchange',
|
||||
component: () => import('@/views/data-exchange/DataExchange.vue')
|
||||
},
|
||||
{
|
||||
path: 'workflows',
|
||||
name: 'Workflows',
|
||||
component: () => import('@/views/workflow/WorkflowList.vue')
|
||||
},
|
||||
{
|
||||
path: 'workflow/:id/edit',
|
||||
name: 'WorkflowEdit',
|
||||
component: () => import('@/views/workflow/WorkflowEditor.vue')
|
||||
},
|
||||
{
|
||||
path: 'workflow/:id/instances',
|
||||
name: 'WorkflowInstances',
|
||||
component: () => import('@/views/workflow/WorkflowInstances.vue')
|
||||
},
|
||||
{
|
||||
path: 'analytics/realtime',
|
||||
name: 'RealtimeAnalytics',
|
||||
component: () => import('@/views/analytics/RealtimeDashboard.vue')
|
||||
},
|
||||
{
|
||||
path: 'analytics/reports',
|
||||
name: 'AnalyticsReports',
|
||||
component: () => import('@/views/analytics/Reports.vue')
|
||||
},
|
||||
{
|
||||
path: 'webhooks',
|
||||
name: 'Webhooks',
|
||||
component: () => import('@/views/webhooks/WebhookList.vue')
|
||||
},
|
||||
{
|
||||
path: 'templates',
|
||||
name: 'Templates',
|
||||
component: () => import('@/views/templates/TemplateList.vue')
|
||||
},
|
||||
{
|
||||
path: 'translations',
|
||||
name: 'Translations',
|
||||
component: () => import('@/views/i18n/TranslationManager.vue')
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'UserManagement',
|
||||
component: () => import('@/views/users/UserManagement.vue')
|
||||
},
|
||||
{
|
||||
path: 'campaigns/schedules',
|
||||
name: 'ScheduledCampaigns',
|
||||
component: () => import('@/views/campaigns/ScheduledCampaigns.vue')
|
||||
},
|
||||
{
|
||||
path: 'campaigns/schedule/new',
|
||||
name: 'CreateSchedule',
|
||||
component: () => import('@/views/campaigns/CampaignScheduler.vue')
|
||||
},
|
||||
{
|
||||
path: 'campaigns/schedule/:id',
|
||||
name: 'EditSchedule',
|
||||
component: () => import('@/views/campaigns/CampaignScheduler.vue')
|
||||
},
|
||||
{
|
||||
path: 'tenant/settings',
|
||||
name: 'TenantSettings',
|
||||
component: () => import('@/views/tenant/TenantSettings.vue'),
|
||||
meta: { requiresRole: 'admin' }
|
||||
},
|
||||
{
|
||||
path: 'tenants',
|
||||
name: 'TenantList',
|
||||
component: () => import('@/views/tenant/TenantList.vue'),
|
||||
meta: { requiresRole: 'superadmin' }
|
||||
},
|
||||
{
|
||||
path: 'billing',
|
||||
name: 'Billing',
|
||||
component: () => import('@/views/billing/BillingDashboard.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('@/views/NotFound.vue')
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
// Navigation guard
|
||||
router.beforeEach((to, from, next) => {
|
||||
console.log('Navigation:', from.path, '->', to.path)
|
||||
|
||||
// Check if user is authenticated by looking at token in localStorage
|
||||
const token = localStorage.getItem('token')
|
||||
const isAuthenticated = !!token
|
||||
|
||||
console.log('Auth check:', { isAuthenticated, requiresAuth: to.meta.requiresAuth })
|
||||
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
console.log('Redirecting to login...')
|
||||
next({ name: 'Login', query: { redirect: to.fullPath } })
|
||||
} else if (to.name === 'Login' && isAuthenticated) {
|
||||
console.log('User already logged in, redirecting to home...')
|
||||
next({ name: 'Home' })
|
||||
} else {
|
||||
console.log('Proceeding to route...')
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
249
marketing-agent/frontend/src/stores/abTesting.js
Normal file
249
marketing-agent/frontend/src/stores/abTesting.js
Normal file
@@ -0,0 +1,249 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import api from '@/api'
|
||||
|
||||
export const useABTestingStore = defineStore('abTesting', () => {
|
||||
// State
|
||||
const experiments = ref([])
|
||||
const currentExperiment = ref(null)
|
||||
const experimentStatus = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
// Actions
|
||||
const fetchExperiments = async (params = {}) => {
|
||||
try {
|
||||
const response = await api.get('/api/experiments', { params })
|
||||
experiments.value = response.data.experiments
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch experiments:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const fetchExperiment = async (experimentId) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await api.get(`/api/experiments/${experimentId}`)
|
||||
currentExperiment.value = response.data
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch experiment:', error)
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createExperiment = async (experimentData) => {
|
||||
try {
|
||||
const response = await api.post('/api/experiments', experimentData)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to create experiment:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const updateExperiment = async (experimentId, updates) => {
|
||||
try {
|
||||
const response = await api.put(`/api/experiments/${experimentId}`, updates)
|
||||
currentExperiment.value = response.data
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to update experiment:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const deleteExperiment = async (experimentId) => {
|
||||
try {
|
||||
await api.delete(`/api/experiments/${experimentId}`)
|
||||
} catch (error) {
|
||||
console.error('Failed to delete experiment:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const startExperiment = async (experimentId) => {
|
||||
try {
|
||||
const response = await api.post(`/api/experiments/${experimentId}/start`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to start experiment:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const stopExperiment = async (experimentId, reason) => {
|
||||
try {
|
||||
const response = await api.post(`/api/experiments/${experimentId}/stop`, { reason })
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to stop experiment:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const fetchExperimentStatus = async (experimentId) => {
|
||||
try {
|
||||
const response = await api.get(`/api/experiments/${experimentId}/status`)
|
||||
experimentStatus.value = response.data
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch experiment status:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Tracking APIs
|
||||
const allocateUser = async (experimentId, userId, userContext = {}) => {
|
||||
try {
|
||||
const response = await api.post('/api/tracking/allocate', {
|
||||
experimentId,
|
||||
userId,
|
||||
userContext
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to allocate user:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const recordConversion = async (experimentId, userId, value = 1, metadata = {}) => {
|
||||
try {
|
||||
const response = await api.post('/api/tracking/convert', {
|
||||
experimentId,
|
||||
userId,
|
||||
value,
|
||||
metadata
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to record conversion:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const batchAllocate = async (experimentId, users) => {
|
||||
try {
|
||||
const response = await api.post('/api/tracking/batch/allocate', {
|
||||
experimentId,
|
||||
users
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to batch allocate users:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const batchRecordConversions = async (experimentId, conversions) => {
|
||||
try {
|
||||
const response = await api.post('/api/tracking/batch/convert', {
|
||||
experimentId,
|
||||
conversions
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to batch record conversions:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Results APIs
|
||||
const fetchExperimentResults = async (experimentId) => {
|
||||
try {
|
||||
const response = await api.get(`/api/results/${experimentId}`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch experiment results:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const fetchVariantDetails = async (experimentId, variantId) => {
|
||||
try {
|
||||
const response = await api.get(`/api/results/${experimentId}/variants/${variantId}`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch variant details:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSegmentAnalysis = async (experimentId) => {
|
||||
try {
|
||||
const response = await api.get(`/api/results/${experimentId}/segments`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch segment analysis:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const exportResults = async (experimentId, format = 'csv') => {
|
||||
try {
|
||||
const response = await api.get(`/api/results/${experimentId}/export`, {
|
||||
params: { format },
|
||||
responseType: 'blob'
|
||||
})
|
||||
|
||||
// Create download link
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', `experiment-${experimentId}-results.${format}`)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to export results:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Reset store
|
||||
const reset = () => {
|
||||
experiments.value = []
|
||||
currentExperiment.value = null
|
||||
experimentStatus.value = null
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
experiments,
|
||||
currentExperiment,
|
||||
experimentStatus,
|
||||
loading,
|
||||
|
||||
// Actions
|
||||
fetchExperiments,
|
||||
fetchExperiment,
|
||||
createExperiment,
|
||||
updateExperiment,
|
||||
deleteExperiment,
|
||||
startExperiment,
|
||||
stopExperiment,
|
||||
fetchExperimentStatus,
|
||||
|
||||
// Tracking
|
||||
allocateUser,
|
||||
recordConversion,
|
||||
batchAllocate,
|
||||
batchRecordConversions,
|
||||
|
||||
// Results
|
||||
fetchExperimentResults,
|
||||
fetchVariantDetails,
|
||||
fetchSegmentAnalysis,
|
||||
exportResults,
|
||||
|
||||
// Utils
|
||||
reset
|
||||
}
|
||||
})
|
||||
102
marketing-agent/frontend/src/stores/auth.js
Normal file
102
marketing-agent/frontend/src/stores/auth.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import api from '@/api'
|
||||
import router from '@/router'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref(null)
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const loading = ref(false)
|
||||
|
||||
const isAuthenticated = computed(() => !!token.value)
|
||||
|
||||
async function login(credentials) {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await api.auth.login(credentials)
|
||||
const { data } = response
|
||||
|
||||
token.value = data.accessToken
|
||||
user.value = data.user
|
||||
localStorage.setItem('token', data.accessToken)
|
||||
localStorage.setItem('refreshToken', data.refreshToken)
|
||||
api.setAuthToken(data.accessToken)
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.response?.data?.error || 'Login failed'
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function register(userData) {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await api.auth.register(userData)
|
||||
const { token: newToken, user: newUser } = response.data
|
||||
|
||||
token.value = newToken
|
||||
user.value = newUser
|
||||
localStorage.setItem('token', newToken)
|
||||
api.setAuthToken(newToken)
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.response?.data?.error || 'Registration failed'
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await api.auth.logout()
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error)
|
||||
} finally {
|
||||
token.value = ''
|
||||
user.value = null
|
||||
localStorage.removeItem('token')
|
||||
api.setAuthToken('')
|
||||
router.push({ name: 'Login' })
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchProfile() {
|
||||
if (!token.value) return
|
||||
|
||||
try {
|
||||
const response = await api.auth.getProfile()
|
||||
user.value = response.data.user
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profile:', error)
|
||||
if (error.response?.status === 401) {
|
||||
await logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize auth token
|
||||
if (token.value) {
|
||||
api.setAuthToken(token.value)
|
||||
fetchProfile()
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
token,
|
||||
loading,
|
||||
isAuthenticated,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
fetchProfile
|
||||
}
|
||||
})
|
||||
268
marketing-agent/frontend/src/stores/plugins/persistedState.js
Normal file
268
marketing-agent/frontend/src/stores/plugins/persistedState.js
Normal file
@@ -0,0 +1,268 @@
|
||||
// Pinia plugin for persisted state with performance optimizations
|
||||
|
||||
import { debounce } from '@/utils/performance'
|
||||
|
||||
const STORAGE_KEY_PREFIX = 'marketing_agent_'
|
||||
|
||||
/**
|
||||
* Create persisted state plugin for Pinia
|
||||
*/
|
||||
export function createPersistedState(options = {}) {
|
||||
const {
|
||||
storage = localStorage,
|
||||
paths = [],
|
||||
debounceTime = 1000,
|
||||
serializer = JSON,
|
||||
filter = () => true
|
||||
} = options
|
||||
|
||||
return (context) => {
|
||||
const { store } = context
|
||||
const storeId = store.$id
|
||||
const storageKey = STORAGE_KEY_PREFIX + storeId
|
||||
|
||||
// Load initial state
|
||||
try {
|
||||
const savedState = storage.getItem(storageKey)
|
||||
if (savedState) {
|
||||
const parsed = serializer.parse(savedState)
|
||||
// Only restore specified paths or all if paths is empty
|
||||
if (paths.length === 0) {
|
||||
store.$patch(parsed)
|
||||
} else {
|
||||
const filtered = {}
|
||||
paths.forEach(path => {
|
||||
const keys = path.split('.')
|
||||
let source = parsed
|
||||
let target = filtered
|
||||
|
||||
keys.forEach((key, index) => {
|
||||
if (index === keys.length - 1) {
|
||||
if (source && key in source) {
|
||||
target[key] = source[key]
|
||||
}
|
||||
} else {
|
||||
if (!target[key]) target[key] = {}
|
||||
target = target[key]
|
||||
source = source ? source[key] : undefined
|
||||
}
|
||||
})
|
||||
})
|
||||
store.$patch(filtered)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load persisted state for ${storeId}:`, error)
|
||||
}
|
||||
|
||||
// Save state changes with debouncing
|
||||
const saveState = debounce(() => {
|
||||
try {
|
||||
const state = store.$state
|
||||
let toSave = state
|
||||
|
||||
// Filter state if paths are specified
|
||||
if (paths.length > 0) {
|
||||
toSave = {}
|
||||
paths.forEach(path => {
|
||||
const keys = path.split('.')
|
||||
let source = state
|
||||
let target = toSave
|
||||
|
||||
keys.forEach((key, index) => {
|
||||
if (index === keys.length - 1) {
|
||||
if (source && key in source) {
|
||||
target[key] = source[key]
|
||||
}
|
||||
} else {
|
||||
if (!target[key]) target[key] = {}
|
||||
target = target[key]
|
||||
source = source ? source[key] : undefined
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Apply custom filter
|
||||
if (filter(toSave)) {
|
||||
storage.setItem(storageKey, serializer.stringify(toSave))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to save state for ${storeId}:`, error)
|
||||
}
|
||||
}, debounceTime)
|
||||
|
||||
// Subscribe to state changes
|
||||
store.$subscribe((mutation, state) => {
|
||||
saveState()
|
||||
})
|
||||
|
||||
// Subscribe to actions for immediate save on specific actions
|
||||
store.$onAction(({ name, after }) => {
|
||||
// Save immediately after logout or critical actions
|
||||
if (['logout', 'clearData', 'resetStore'].includes(name)) {
|
||||
after(() => {
|
||||
saveState.cancel()
|
||||
try {
|
||||
if (name === 'logout' || name === 'clearData') {
|
||||
storage.removeItem(storageKey)
|
||||
} else {
|
||||
storage.setItem(storageKey, serializer.stringify(store.$state))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to handle action ${name}:`, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Secure storage wrapper with encryption
|
||||
*/
|
||||
export class SecureStorage {
|
||||
constructor(secretKey) {
|
||||
this.secretKey = secretKey
|
||||
}
|
||||
|
||||
async encrypt(data) {
|
||||
const encoder = new TextEncoder()
|
||||
const dataBuffer = encoder.encode(JSON.stringify(data))
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(this.secretKey),
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['encrypt']
|
||||
)
|
||||
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12))
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
dataBuffer
|
||||
)
|
||||
|
||||
const combined = new Uint8Array(iv.length + encrypted.byteLength)
|
||||
combined.set(iv)
|
||||
combined.set(new Uint8Array(encrypted), iv.length)
|
||||
|
||||
return btoa(String.fromCharCode(...combined))
|
||||
}
|
||||
|
||||
async decrypt(encryptedData) {
|
||||
const combined = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0))
|
||||
const iv = combined.slice(0, 12)
|
||||
const encrypted = combined.slice(12)
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(this.secretKey),
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['decrypt']
|
||||
)
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
encrypted
|
||||
)
|
||||
|
||||
const decoder = new TextDecoder()
|
||||
return JSON.parse(decoder.decode(decrypted))
|
||||
}
|
||||
|
||||
setItem(key, value) {
|
||||
return this.encrypt(value).then(encrypted => {
|
||||
localStorage.setItem(key, encrypted)
|
||||
})
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
const encrypted = localStorage.getItem(key)
|
||||
if (!encrypted) return Promise.resolve(null)
|
||||
|
||||
return this.decrypt(encrypted).catch(error => {
|
||||
console.error('Decryption failed:', error)
|
||||
localStorage.removeItem(key)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
removeItem(key) {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* IndexedDB storage for large data
|
||||
*/
|
||||
export class IndexedDBStorage {
|
||||
constructor(dbName = 'marketing_agent_store', storeName = 'state') {
|
||||
this.dbName = dbName
|
||||
this.storeName = storeName
|
||||
this.db = null
|
||||
}
|
||||
|
||||
async init() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, 1)
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result
|
||||
resolve()
|
||||
}
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
db.createObjectStore(this.storeName)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async setItem(key, value) {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readwrite')
|
||||
const store = transaction.objectStore(this.storeName)
|
||||
const request = store.put(value, key)
|
||||
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async getItem(key) {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readonly')
|
||||
const store = transaction.objectStore(this.storeName)
|
||||
const request = store.get(key)
|
||||
|
||||
request.onsuccess = () => resolve(request.result)
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async removeItem(key) {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readwrite')
|
||||
const store = transaction.objectStore(this.storeName)
|
||||
const request = store.delete(key)
|
||||
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
84
marketing-agent/frontend/src/style.css
Normal file
84
marketing-agent/frontend/src/style.css
Normal file
@@ -0,0 +1,84 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--el-color-primary: #06b6d4;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Element Plus customizations */
|
||||
.el-button--primary {
|
||||
--el-button-bg-color: #06b6d4;
|
||||
--el-button-border-color: #06b6d4;
|
||||
--el-button-hover-bg-color: #0891b2;
|
||||
--el-button-hover-border-color: #0891b2;
|
||||
}
|
||||
|
||||
/* Chart container */
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Loading animation */
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(6, 182, 212, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #06b6d4;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Fade transition */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
192
marketing-agent/frontend/src/styles/mobile.css
Normal file
192
marketing-agent/frontend/src/styles/mobile.css
Normal file
@@ -0,0 +1,192 @@
|
||||
/* Mobile-specific styles for Marketing Agent System */
|
||||
|
||||
/* Ensure full height on mobile browsers */
|
||||
.h-full {
|
||||
height: 100vh;
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
/* Fix iOS Safari bottom bar issue */
|
||||
.pb-safe {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* Mobile-optimized form elements */
|
||||
@media (max-width: 768px) {
|
||||
/* Larger touch targets */
|
||||
.el-button {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
height: 44px !important;
|
||||
font-size: 16px !important; /* Prevent zoom on iOS */
|
||||
}
|
||||
|
||||
.el-select .el-input__inner {
|
||||
height: 44px !important;
|
||||
}
|
||||
|
||||
/* Better spacing for mobile */
|
||||
.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Mobile-friendly tables */
|
||||
.el-table {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.el-table .cell {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
/* Responsive cards */
|
||||
.el-card {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* Mobile dialogs */
|
||||
.el-dialog {
|
||||
width: 90% !important;
|
||||
margin-top: 10vh !important;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
/* Mobile message boxes */
|
||||
.el-message-box {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
/* Mobile dropdowns */
|
||||
.el-dropdown-menu {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
/* Bottom navigation spacing */
|
||||
.pb-20 {
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth scrolling */
|
||||
.scroll-smooth {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Prevent text selection on interactive elements */
|
||||
.no-select {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Mobile tap highlight */
|
||||
.tap-highlight {
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pull to refresh indicator */
|
||||
.pull-to-refresh {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: top 0.3s;
|
||||
}
|
||||
|
||||
.pull-to-refresh.active {
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
/* Mobile-optimized animations */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Swipe actions */
|
||||
.swipe-action {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
.swipe-action-content {
|
||||
position: relative;
|
||||
background: white;
|
||||
z-index: 2;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.swipe-action-buttons {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Mobile-friendly tooltips */
|
||||
@media (max-width: 768px) {
|
||||
.el-tooltip__popper {
|
||||
max-width: 250px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive images */
|
||||
.responsive-img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Mobile grid adjustments */
|
||||
@media (max-width: 640px) {
|
||||
.grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-cols-3 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
34
marketing-agent/frontend/src/utils/date.js
Normal file
34
marketing-agent/frontend/src/utils/date.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { format, parseISO } from 'date-fns'
|
||||
|
||||
export function formatDateTime(dateString) {
|
||||
if (!dateString) return '-'
|
||||
try {
|
||||
const date = typeof dateString === 'string' ? parseISO(dateString) : dateString
|
||||
return format(date, 'yyyy-MM-dd HH:mm:ss')
|
||||
} catch (error) {
|
||||
console.error('Date formatting error:', error)
|
||||
return dateString
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDate(dateString) {
|
||||
if (!dateString) return '-'
|
||||
try {
|
||||
const date = typeof dateString === 'string' ? parseISO(dateString) : dateString
|
||||
return format(date, 'yyyy-MM-dd')
|
||||
} catch (error) {
|
||||
console.error('Date formatting error:', error)
|
||||
return dateString
|
||||
}
|
||||
}
|
||||
|
||||
export function formatTime(dateString) {
|
||||
if (!dateString) return '-'
|
||||
try {
|
||||
const date = typeof dateString === 'string' ? parseISO(dateString) : dateString
|
||||
return format(date, 'HH:mm:ss')
|
||||
} catch (error) {
|
||||
console.error('Date formatting error:', error)
|
||||
return dateString
|
||||
}
|
||||
}
|
||||
321
marketing-agent/frontend/src/utils/performance.js
Normal file
321
marketing-agent/frontend/src/utils/performance.js
Normal file
@@ -0,0 +1,321 @@
|
||||
// Performance monitoring and optimization utilities
|
||||
|
||||
/**
|
||||
* Performance Observer for monitoring various metrics
|
||||
*/
|
||||
export class PerformanceMonitor {
|
||||
constructor() {
|
||||
this.metrics = {
|
||||
fcp: null, // First Contentful Paint
|
||||
lcp: null, // Largest Contentful Paint
|
||||
fid: null, // First Input Delay
|
||||
cls: null, // Cumulative Layout Shift
|
||||
ttfb: null, // Time to First Byte
|
||||
tti: null, // Time to Interactive
|
||||
resourceTimings: []
|
||||
}
|
||||
|
||||
this.initializeObservers()
|
||||
}
|
||||
|
||||
initializeObservers() {
|
||||
// Web Vitals Observer
|
||||
if ('PerformanceObserver' in window) {
|
||||
// LCP Observer
|
||||
const lcpObserver = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries()
|
||||
const lastEntry = entries[entries.length - 1]
|
||||
this.metrics.lcp = lastEntry.renderTime || lastEntry.loadTime
|
||||
})
|
||||
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] })
|
||||
|
||||
// FID Observer
|
||||
const fidObserver = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries()
|
||||
entries.forEach((entry) => {
|
||||
this.metrics.fid = entry.processingStart - entry.startTime
|
||||
})
|
||||
})
|
||||
fidObserver.observe({ entryTypes: ['first-input'] })
|
||||
|
||||
// CLS Observer
|
||||
let clsValue = 0
|
||||
let clsEntries = []
|
||||
const clsObserver = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
if (!entry.hadRecentInput) {
|
||||
clsValue += entry.value
|
||||
clsEntries.push(entry)
|
||||
}
|
||||
}
|
||||
this.metrics.cls = clsValue
|
||||
})
|
||||
clsObserver.observe({ entryTypes: ['layout-shift'] })
|
||||
|
||||
// Navigation Timing
|
||||
if (window.performance && window.performance.timing) {
|
||||
const timing = window.performance.timing
|
||||
this.metrics.ttfb = timing.responseStart - timing.fetchStart
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current performance metrics
|
||||
*/
|
||||
getMetrics() {
|
||||
// Get FCP from performance API
|
||||
const paintMetrics = performance.getEntriesByType('paint')
|
||||
const fcp = paintMetrics.find(metric => metric.name === 'first-contentful-paint')
|
||||
if (fcp) {
|
||||
this.metrics.fcp = fcp.startTime
|
||||
}
|
||||
|
||||
return this.metrics
|
||||
}
|
||||
|
||||
/**
|
||||
* Report metrics to analytics service
|
||||
*/
|
||||
async reportMetrics() {
|
||||
const metrics = this.getMetrics()
|
||||
|
||||
try {
|
||||
await fetch('/api/v1/analytics/performance', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: window.location.href,
|
||||
userAgent: navigator.userAgent,
|
||||
timestamp: new Date().toISOString(),
|
||||
metrics
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to report performance metrics:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor long tasks
|
||||
*/
|
||||
monitorLongTasks() {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
console.warn('Long task detected:', {
|
||||
duration: entry.duration,
|
||||
startTime: entry.startTime,
|
||||
name: entry.name
|
||||
})
|
||||
}
|
||||
})
|
||||
observer.observe({ entryTypes: ['longtask'] })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy loading utility for components
|
||||
*/
|
||||
export function lazyLoadComponent(componentPath) {
|
||||
return () => ({
|
||||
component: import(componentPath),
|
||||
loading: () => import('@/components/common/LoadingComponent.vue'),
|
||||
error: () => import('@/components/common/ErrorComponent.vue'),
|
||||
delay: 200,
|
||||
timeout: 10000
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce function for performance optimization
|
||||
*/
|
||||
export function debounce(func, wait, immediate = false) {
|
||||
let timeout
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
timeout = null
|
||||
if (!immediate) func(...args)
|
||||
}
|
||||
const callNow = immediate && !timeout
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(later, wait)
|
||||
if (callNow) func(...args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttle function for performance optimization
|
||||
*/
|
||||
export function throttle(func, limit) {
|
||||
let inThrottle
|
||||
return function(...args) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args)
|
||||
inThrottle = true
|
||||
setTimeout(() => inThrottle = false, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual scrolling helper
|
||||
*/
|
||||
export class VirtualScroller {
|
||||
constructor(options) {
|
||||
this.itemHeight = options.itemHeight
|
||||
this.containerHeight = options.containerHeight
|
||||
this.items = options.items || []
|
||||
this.buffer = options.buffer || 5
|
||||
}
|
||||
|
||||
getVisibleItems(scrollTop) {
|
||||
const startIndex = Math.floor(scrollTop / this.itemHeight)
|
||||
const endIndex = Math.ceil((scrollTop + this.containerHeight) / this.itemHeight)
|
||||
|
||||
const visibleStartIndex = Math.max(0, startIndex - this.buffer)
|
||||
const visibleEndIndex = Math.min(this.items.length, endIndex + this.buffer)
|
||||
|
||||
return {
|
||||
items: this.items.slice(visibleStartIndex, visibleEndIndex),
|
||||
startIndex: visibleStartIndex,
|
||||
endIndex: visibleEndIndex,
|
||||
totalHeight: this.items.length * this.itemHeight,
|
||||
offsetY: visibleStartIndex * this.itemHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Image lazy loading
|
||||
*/
|
||||
export function setupImageLazyLoading() {
|
||||
if ('IntersectionObserver' in window) {
|
||||
const imageObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target
|
||||
img.src = img.dataset.src
|
||||
img.classList.add('loaded')
|
||||
imageObserver.unobserve(img)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const lazyImages = document.querySelectorAll('img[data-src]')
|
||||
lazyImages.forEach(img => imageObserver.observe(img))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request idle callback polyfill
|
||||
*/
|
||||
export const requestIdleCallback = window.requestIdleCallback || function(cb) {
|
||||
const start = Date.now()
|
||||
return setTimeout(() => {
|
||||
cb({
|
||||
didTimeout: false,
|
||||
timeRemaining: () => Math.max(0, 50 - (Date.now() - start))
|
||||
})
|
||||
}, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel idle callback polyfill
|
||||
*/
|
||||
export const cancelIdleCallback = window.cancelIdleCallback || function(id) {
|
||||
clearTimeout(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch DOM updates
|
||||
*/
|
||||
export class DOMBatcher {
|
||||
constructor() {
|
||||
this.reads = []
|
||||
this.writes = []
|
||||
this.scheduled = false
|
||||
}
|
||||
|
||||
read(fn) {
|
||||
this.reads.push(fn)
|
||||
this.scheduleFlush()
|
||||
}
|
||||
|
||||
write(fn) {
|
||||
this.writes.push(fn)
|
||||
this.scheduleFlush()
|
||||
}
|
||||
|
||||
scheduleFlush() {
|
||||
if (!this.scheduled) {
|
||||
this.scheduled = true
|
||||
requestAnimationFrame(() => this.flush())
|
||||
}
|
||||
}
|
||||
|
||||
flush() {
|
||||
const reads = this.reads.slice()
|
||||
const writes = this.writes.slice()
|
||||
|
||||
this.reads.length = 0
|
||||
this.writes.length = 0
|
||||
this.scheduled = false
|
||||
|
||||
// Execute reads first
|
||||
reads.forEach(fn => fn())
|
||||
// Then execute writes
|
||||
writes.forEach(fn => fn())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Memory leak detector
|
||||
*/
|
||||
export class MemoryLeakDetector {
|
||||
constructor() {
|
||||
this.observers = new WeakMap()
|
||||
this.checkInterval = null
|
||||
}
|
||||
|
||||
observe(object, name) {
|
||||
this.observers.set(object, {
|
||||
name,
|
||||
timestamp: Date.now(),
|
||||
size: JSON.stringify(object).length
|
||||
})
|
||||
}
|
||||
|
||||
startChecking(interval = 60000) {
|
||||
this.checkInterval = setInterval(() => {
|
||||
if (performance.memory) {
|
||||
const memoryInfo = {
|
||||
usedJSHeapSize: performance.memory.usedJSHeapSize,
|
||||
totalJSHeapSize: performance.memory.totalJSHeapSize,
|
||||
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
|
||||
}
|
||||
|
||||
const usage = (memoryInfo.usedJSHeapSize / memoryInfo.jsHeapSizeLimit) * 100
|
||||
|
||||
if (usage > 90) {
|
||||
console.warn('High memory usage detected:', usage.toFixed(2) + '%')
|
||||
}
|
||||
}
|
||||
}, interval)
|
||||
}
|
||||
|
||||
stopChecking() {
|
||||
if (this.checkInterval) {
|
||||
clearInterval(this.checkInterval)
|
||||
this.checkInterval = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instances
|
||||
export const performanceMonitor = new PerformanceMonitor()
|
||||
export const domBatcher = new DOMBatcher()
|
||||
export const memoryLeakDetector = new MemoryLeakDetector()
|
||||
279
marketing-agent/frontend/src/utils/serviceWorker.js
Normal file
279
marketing-agent/frontend/src/utils/serviceWorker.js
Normal file
@@ -0,0 +1,279 @@
|
||||
// Service Worker registration and management
|
||||
|
||||
/**
|
||||
* Register service worker
|
||||
*/
|
||||
export async function registerServiceWorker() {
|
||||
if (!('serviceWorker' in navigator)) {
|
||||
console.log('Service Worker not supported')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register('/service-worker.js', {
|
||||
scope: '/'
|
||||
})
|
||||
|
||||
console.log('Service Worker registered:', registration)
|
||||
|
||||
// Handle updates
|
||||
registration.addEventListener('updatefound', () => {
|
||||
const newWorker = registration.installing
|
||||
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
// New service worker available
|
||||
showUpdateNotification(newWorker)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Check for updates periodically
|
||||
setInterval(() => {
|
||||
registration.update()
|
||||
}, 60 * 60 * 1000) // Every hour
|
||||
|
||||
return registration
|
||||
} catch (error) {
|
||||
console.error('Service Worker registration failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show update notification
|
||||
*/
|
||||
function showUpdateNotification(worker) {
|
||||
const notification = document.createElement('div')
|
||||
notification.className = 'sw-update-notification'
|
||||
notification.innerHTML = `
|
||||
<div class="sw-update-content">
|
||||
<p>A new version is available!</p>
|
||||
<button id="sw-update-btn">Update</button>
|
||||
<button id="sw-dismiss-btn">Dismiss</button>
|
||||
</div>
|
||||
`
|
||||
|
||||
document.body.appendChild(notification)
|
||||
|
||||
document.getElementById('sw-update-btn').addEventListener('click', () => {
|
||||
worker.postMessage({ type: 'SKIP_WAITING' })
|
||||
window.location.reload()
|
||||
})
|
||||
|
||||
document.getElementById('sw-dismiss-btn').addEventListener('click', () => {
|
||||
notification.remove()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Request notification permission
|
||||
*/
|
||||
export async function requestNotificationPermission() {
|
||||
if (!('Notification' in window)) {
|
||||
console.log('Notifications not supported')
|
||||
return false
|
||||
}
|
||||
|
||||
if (Notification.permission === 'granted') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (Notification.permission !== 'denied') {
|
||||
const permission = await Notification.requestPermission()
|
||||
return permission === 'granted'
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to push notifications
|
||||
*/
|
||||
export async function subscribeToPushNotifications(registration) {
|
||||
try {
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(process.env.VITE_VAPID_PUBLIC_KEY)
|
||||
})
|
||||
|
||||
// Send subscription to server
|
||||
await fetch('/api/v1/notifications/subscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(subscription)
|
||||
})
|
||||
|
||||
return subscription
|
||||
} catch (error) {
|
||||
console.error('Failed to subscribe to push notifications:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from push notifications
|
||||
*/
|
||||
export async function unsubscribeFromPushNotifications() {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.ready
|
||||
const subscription = await registration.pushManager.getSubscription()
|
||||
|
||||
if (subscription) {
|
||||
await subscription.unsubscribe()
|
||||
|
||||
// Notify server
|
||||
await fetch('/api/v1/notifications/unsubscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ endpoint: subscription.endpoint })
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to unsubscribe from push notifications:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all caches
|
||||
*/
|
||||
export async function clearAllCaches() {
|
||||
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
|
||||
navigator.serviceWorker.controller.postMessage({ type: 'CLEAR_CACHE' })
|
||||
}
|
||||
|
||||
// Also clear caches directly
|
||||
if ('caches' in window) {
|
||||
const cacheNames = await caches.keys()
|
||||
await Promise.all(cacheNames.map(name => caches.delete(name)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if app is offline
|
||||
*/
|
||||
export function isOffline() {
|
||||
return !navigator.onLine
|
||||
}
|
||||
|
||||
/**
|
||||
* Add offline/online event listeners
|
||||
*/
|
||||
export function setupNetworkListeners(callbacks = {}) {
|
||||
const { onOnline, onOffline } = callbacks
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
console.log('App is online')
|
||||
if (onOnline) onOnline()
|
||||
})
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
console.log('App is offline')
|
||||
if (onOffline) onOffline()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Background sync
|
||||
*/
|
||||
export async function registerBackgroundSync(tag) {
|
||||
const registration = await navigator.serviceWorker.ready
|
||||
|
||||
if ('sync' in registration) {
|
||||
try {
|
||||
await registration.sync.register(tag)
|
||||
console.log(`Background sync registered: ${tag}`)
|
||||
} catch (error) {
|
||||
console.error('Background sync registration failed:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to convert VAPID key
|
||||
*/
|
||||
function urlBase64ToUint8Array(base64String) {
|
||||
const padding = '='.repeat((4 - base64String.length % 4) % 4)
|
||||
const base64 = (base64String + padding)
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/')
|
||||
|
||||
const rawData = window.atob(base64)
|
||||
const outputArray = new Uint8Array(rawData.length)
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i)
|
||||
}
|
||||
|
||||
return outputArray
|
||||
}
|
||||
|
||||
// Add CSS for update notification
|
||||
const style = document.createElement('style')
|
||||
style.textContent = `
|
||||
.sw-update-notification {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
padding: 16px;
|
||||
z-index: 9999;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.sw-update-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sw-update-content p {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sw-update-content button {
|
||||
padding: 6px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
#sw-update-btn {
|
||||
background: #409eff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#sw-update-btn:hover {
|
||||
background: #66b1ff;
|
||||
}
|
||||
|
||||
#sw-dismiss-btn {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#sw-dismiss-btn:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
201
marketing-agent/frontend/src/views/ABTesting.vue
Normal file
201
marketing-agent/frontend/src/views/ABTesting.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div class="abtesting-page">
|
||||
<el-card class="page-header">
|
||||
<h1>{{ $t('abtesting.title') }}</h1>
|
||||
<p>{{ $t('abtesting.subtitle') }}</p>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('abtesting.experiments') }}</span>
|
||||
<el-button type="primary" @click="showCreateDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
{{ $t('abtesting.createExperiment') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="experiments" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="name" :label="$t('abtesting.name')" />
|
||||
<el-table-column prop="status" :label="$t('abtesting.status')">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
{{ row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="variants" :label="$t('abtesting.variants')">
|
||||
<template #default="{ row }">
|
||||
{{ row.variants.length }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('abtesting.progress')">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="row.progress" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('abtesting.actions')" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="viewDetails(row)">
|
||||
{{ $t('common.view') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="success"
|
||||
v-if="row.status === 'completed'"
|
||||
@click="selectWinner(row)"
|
||||
>
|
||||
{{ $t('abtesting.selectWinner') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- Create Experiment Dialog -->
|
||||
<el-dialog
|
||||
v-model="showCreateDialog"
|
||||
:title="$t('abtesting.createExperiment')"
|
||||
width="600px"
|
||||
>
|
||||
<el-form :model="newExperiment" label-width="120px">
|
||||
<el-form-item :label="$t('abtesting.name')">
|
||||
<el-input v-model="newExperiment.name" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('abtesting.description')">
|
||||
<el-input type="textarea" v-model="newExperiment.description" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('abtesting.variants')">
|
||||
<div v-for="(variant, index) in newExperiment.variants" :key="index" class="variant-item">
|
||||
<el-input v-model="variant.name" placeholder="Variant name" />
|
||||
<el-input v-model="variant.content" type="textarea" placeholder="Content" />
|
||||
<el-button @click="removeVariant(index)" type="danger" :icon="Delete" />
|
||||
</div>
|
||||
<el-button @click="addVariant" type="primary">
|
||||
{{ $t('abtesting.addVariant') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showCreateDialog = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="createExperiment">
|
||||
{{ $t('common.create') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Plus, Delete } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import api from '@/api'
|
||||
|
||||
const loading = ref(false)
|
||||
const experiments = ref([])
|
||||
const showCreateDialog = ref(false)
|
||||
const newExperiment = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
variants: [
|
||||
{ name: 'Control', content: '' },
|
||||
{ name: 'Variant A', content: '' }
|
||||
]
|
||||
})
|
||||
|
||||
const loadExperiments = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await api.abTesting.getExperiments()
|
||||
experiments.value = response.data
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to load experiments')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createExperiment = async () => {
|
||||
try {
|
||||
await api.abTesting.createExperiment(newExperiment.value)
|
||||
ElMessage.success('Experiment created successfully')
|
||||
showCreateDialog.value = false
|
||||
loadExperiments()
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to create experiment')
|
||||
}
|
||||
}
|
||||
|
||||
const addVariant = () => {
|
||||
newExperiment.value.variants.push({
|
||||
name: `Variant ${String.fromCharCode(65 + newExperiment.value.variants.length - 1)}`,
|
||||
content: ''
|
||||
})
|
||||
}
|
||||
|
||||
const removeVariant = (index) => {
|
||||
if (newExperiment.value.variants.length > 2) {
|
||||
newExperiment.value.variants.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const viewDetails = (experiment) => {
|
||||
// Navigate to experiment details
|
||||
}
|
||||
|
||||
const selectWinner = async (experiment) => {
|
||||
// Select winner implementation
|
||||
}
|
||||
|
||||
const getStatusType = (status) => {
|
||||
const types = {
|
||||
draft: 'info',
|
||||
running: 'primary',
|
||||
completed: 'success',
|
||||
cancelled: 'danger'
|
||||
}
|
||||
return types[status] || 'info'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadExperiments()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.abtesting-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h1 {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.variant-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
111
marketing-agent/frontend/src/views/Accounts.vue
Normal file
111
marketing-agent/frontend/src/views/Accounts.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="accounts-page">
|
||||
<el-card class="page-header">
|
||||
<h1>{{ $t('accounts.title') }}</h1>
|
||||
<p>{{ $t('accounts.subtitle') }}</p>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-card>
|
||||
<el-table :data="accounts" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="phoneNumber" :label="$t('accounts.phoneNumber')" />
|
||||
<el-table-column prop="status" :label="$t('accounts.status')">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
{{ row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="lastUsed" :label="$t('accounts.lastUsed')" />
|
||||
<el-table-column prop="messagesSent" :label="$t('accounts.messagesSent')" />
|
||||
<el-table-column :label="$t('accounts.actions')" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="viewDetails(row)">
|
||||
{{ $t('common.view') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:type="row.status === 'active' ? 'danger' : 'success'"
|
||||
@click="toggleStatus(row)"
|
||||
>
|
||||
{{ row.status === 'active' ? $t('common.disable') : $t('common.enable') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import api from '@/api'
|
||||
|
||||
const loading = ref(false)
|
||||
const accounts = ref([])
|
||||
|
||||
const loadAccounts = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await api.accounts.getList()
|
||||
accounts.value = response.data
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to load accounts')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusType = (status) => {
|
||||
const types = {
|
||||
active: 'success',
|
||||
inactive: 'info',
|
||||
banned: 'danger',
|
||||
restricted: 'warning'
|
||||
}
|
||||
return types[status] || 'info'
|
||||
}
|
||||
|
||||
const viewDetails = (account) => {
|
||||
// Navigate to account details
|
||||
}
|
||||
|
||||
const toggleStatus = async (account) => {
|
||||
try {
|
||||
const newStatus = account.status === 'active' ? 'inactive' : 'active'
|
||||
await api.accounts.updateStatus(account.id, newStatus)
|
||||
ElMessage.success('Account status updated')
|
||||
loadAccounts()
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to update account status')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAccounts()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.accounts-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h1 {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
348
marketing-agent/frontend/src/views/AccountsEnhanced.vue
Normal file
348
marketing-agent/frontend/src/views/AccountsEnhanced.vue
Normal file
@@ -0,0 +1,348 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold">Telegram Accounts</h1>
|
||||
<el-button type="primary" @click="showConnectDialog = true">
|
||||
<el-icon class="mr-2"><Plus /></el-icon>
|
||||
Connect Account
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- Accounts List -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<el-card v-for="account in accounts" :key="account.id" shadow="hover">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center mb-2">
|
||||
<el-tag :type="account.connected ? 'success' : 'info'" size="small">
|
||||
{{ account.connected ? 'Connected' : 'Inactive' }}
|
||||
</el-tag>
|
||||
<el-tag v-if="account.isDemo" type="warning" size="small" class="ml-2">
|
||||
Demo
|
||||
</el-tag>
|
||||
</div>
|
||||
<h3 class="font-semibold text-lg">{{ account.username || 'Unknown' }}</h3>
|
||||
<p class="text-gray-500">{{ account.phone }}</p>
|
||||
<div class="mt-2 text-sm text-gray-600">
|
||||
<p>Messages sent: {{ account.messageCount || 0 }}</p>
|
||||
<p>Last active: {{ formatTime(account.lastActive) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<el-dropdown @command="(cmd) => handleCommand(cmd, account)">
|
||||
<el-button circle size="small">
|
||||
<el-icon><MoreFilled /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="!account.connected" command="reconnect">
|
||||
Reconnect
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="status">View Status</el-dropdown-item>
|
||||
<el-dropdown-item command="disconnect" divided>
|
||||
<span class="text-red-500">Disconnect</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="accounts.length === 0" class="col-span-full text-center py-12">
|
||||
<el-empty description="No accounts connected">
|
||||
<el-button type="primary" @click="showConnectDialog = true">
|
||||
Connect First Account
|
||||
</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connect Account Dialog -->
|
||||
<el-dialog
|
||||
v-model="showConnectDialog"
|
||||
title="Connect Telegram Account"
|
||||
width="500px"
|
||||
@close="resetConnectForm"
|
||||
>
|
||||
<div v-if="!authStep">
|
||||
<el-form ref="connectFormRef" :model="connectForm" :rules="connectRules" label-position="top">
|
||||
<el-form-item label="Phone Number" prop="phone">
|
||||
<el-input
|
||||
v-model="connectForm.phone"
|
||||
placeholder="+1234567890"
|
||||
prefix-icon="Phone"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="connectForm.demo">
|
||||
Create demo account (for testing)
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div v-else-if="authStep === 'code'">
|
||||
<el-alert type="info" :closable="false" class="mb-4">
|
||||
Please check your Telegram app for the authentication code
|
||||
</el-alert>
|
||||
<el-form ref="verifyFormRef" :model="verifyForm" label-position="top">
|
||||
<el-form-item label="Verification Code" prop="code">
|
||||
<el-input
|
||||
v-model="verifyForm.code"
|
||||
placeholder="12345"
|
||||
maxlength="5"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="requiresPassword" label="2FA Password" prop="password">
|
||||
<el-input
|
||||
v-model="verifyForm.password"
|
||||
type="password"
|
||||
placeholder="Your 2FA password"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showConnectDialog = false">Cancel</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="connecting"
|
||||
@click="authStep ? verifyCode() : connectAccount()"
|
||||
>
|
||||
{{ authStep ? 'Verify' : 'Connect' }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Status Dialog -->
|
||||
<el-dialog
|
||||
v-model="showStatusDialog"
|
||||
title="Account Status"
|
||||
width="500px"
|
||||
>
|
||||
<el-descriptions v-if="selectedAccount" :column="1" border>
|
||||
<el-descriptions-item label="Account ID">
|
||||
{{ selectedAccount.accountId }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Phone">
|
||||
{{ selectedAccount.phone }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Username">
|
||||
{{ selectedAccount.username || 'N/A' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Status">
|
||||
<el-tag :type="selectedAccount.connected ? 'success' : 'info'">
|
||||
{{ selectedAccount.connected ? 'Connected' : 'Inactive' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Messages Sent">
|
||||
{{ selectedAccount.messageCount || 0 }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Last Active">
|
||||
{{ formatTime(selectedAccount.lastActive) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Uptime">
|
||||
{{ formatUptime(selectedAccount.uptime) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Phone, MoreFilled } from '@element-plus/icons-vue'
|
||||
import api from '@/api'
|
||||
|
||||
const accounts = ref([])
|
||||
const showConnectDialog = ref(false)
|
||||
const showStatusDialog = ref(false)
|
||||
const selectedAccount = ref(null)
|
||||
const connecting = ref(false)
|
||||
const authStep = ref('')
|
||||
const pendingAccountId = ref('')
|
||||
const requiresPassword = ref(false)
|
||||
|
||||
const connectForm = reactive({
|
||||
phone: '',
|
||||
demo: false
|
||||
})
|
||||
|
||||
const verifyForm = reactive({
|
||||
code: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const connectRules = {
|
||||
phone: [
|
||||
{ required: true, message: 'Please enter phone number', trigger: 'blur' },
|
||||
{ pattern: /^\+?[1-9]\d{1,14}$/, message: 'Invalid phone number format', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const connectFormRef = ref()
|
||||
const verifyFormRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
loadAccounts()
|
||||
})
|
||||
|
||||
const loadAccounts = async () => {
|
||||
try {
|
||||
const response = await api.accounts.getAccounts()
|
||||
accounts.value = response.data.data || []
|
||||
} catch (error) {
|
||||
console.error('Failed to load accounts:', error)
|
||||
ElMessage.error('Failed to load accounts')
|
||||
}
|
||||
}
|
||||
|
||||
const connectAccount = async () => {
|
||||
const valid = await connectFormRef.value.validate()
|
||||
if (!valid) return
|
||||
|
||||
connecting.value = true
|
||||
|
||||
try {
|
||||
const response = await api.accounts.connectAccount({
|
||||
phone: connectForm.phone,
|
||||
demo: connectForm.demo
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
if (response.data.data.demo) {
|
||||
ElMessage.success('Demo account created successfully')
|
||||
showConnectDialog.value = false
|
||||
loadAccounts()
|
||||
} else {
|
||||
// Need authentication
|
||||
pendingAccountId.value = response.data.data.accountId
|
||||
authStep.value = 'code'
|
||||
ElMessage.info(response.data.data.message)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to connect account:', error)
|
||||
ElMessage.error(error.response?.data?.error || 'Failed to connect account')
|
||||
} finally {
|
||||
connecting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const verifyCode = async () => {
|
||||
if (!verifyForm.code) {
|
||||
ElMessage.warning('Please enter verification code')
|
||||
return
|
||||
}
|
||||
|
||||
connecting.value = true
|
||||
|
||||
try {
|
||||
const response = await api.accounts.verifyAccount(pendingAccountId.value, {
|
||||
code: verifyForm.code,
|
||||
password: verifyForm.password
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
ElMessage.success('Account connected successfully')
|
||||
showConnectDialog.value = false
|
||||
loadAccounts()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Verification failed:', error)
|
||||
|
||||
if (error.response?.data?.requiresPassword) {
|
||||
requiresPassword.value = true
|
||||
ElMessage.warning('2FA password required')
|
||||
} else {
|
||||
ElMessage.error(error.response?.data?.error || 'Verification failed')
|
||||
}
|
||||
} finally {
|
||||
connecting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCommand = async (command, account) => {
|
||||
switch (command) {
|
||||
case 'status':
|
||||
try {
|
||||
const response = await api.accounts.getAccountStatus(account.id)
|
||||
selectedAccount.value = response.data.data
|
||||
showStatusDialog.value = true
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to get account status')
|
||||
}
|
||||
break
|
||||
|
||||
case 'reconnect':
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'Reconnect this account?',
|
||||
'Confirm',
|
||||
{ type: 'info' }
|
||||
)
|
||||
|
||||
const response = await api.accounts.reconnectAccount(account.id)
|
||||
if (response.data.success) {
|
||||
ElMessage.success('Account reconnected successfully')
|
||||
loadAccounts()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('Failed to reconnect account')
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case 'disconnect':
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'Are you sure you want to disconnect this account?',
|
||||
'Warning',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
|
||||
await api.accounts.disconnectAccount(account.id)
|
||||
ElMessage.success('Account disconnected')
|
||||
loadAccounts()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('Failed to disconnect account')
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const resetConnectForm = () => {
|
||||
connectForm.phone = ''
|
||||
connectForm.demo = false
|
||||
verifyForm.code = ''
|
||||
verifyForm.password = ''
|
||||
authStep.value = ''
|
||||
pendingAccountId.value = ''
|
||||
requiresPassword.value = false
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
if (!timestamp) return 'Never'
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
const formatUptime = (uptime) => {
|
||||
if (!uptime) return 'N/A'
|
||||
|
||||
const hours = Math.floor(uptime / 3600000)
|
||||
const minutes = Math.floor((uptime % 3600000) / 60000)
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`
|
||||
}
|
||||
return `${minutes}m`
|
||||
}
|
||||
</script>
|
||||
416
marketing-agent/frontend/src/views/Analytics.vue
Normal file
416
marketing-agent/frontend/src/views/Analytics.vue
Normal file
@@ -0,0 +1,416 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-semibold text-gray-900">{{ t('analytics.overview') }}</h1>
|
||||
<div class="flex items-center space-x-3">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="to"
|
||||
start-placeholder="Start date"
|
||||
end-placeholder="End date"
|
||||
@change="loadAnalytics"
|
||||
/>
|
||||
<el-button icon="Download" @click="exportReport">
|
||||
Export Report
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Metrics -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<el-card v-for="metric in keyMetrics" :key="metric.title">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">{{ metric.title }}</p>
|
||||
<p class="mt-2 text-2xl font-semibold">{{ metric.value }}</p>
|
||||
<div class="mt-1 flex items-center text-sm">
|
||||
<el-icon :class="metric.trend > 0 ? 'text-green-500' : 'text-red-500'">
|
||||
<component :is="metric.trend > 0 ? 'TrendCharts' : 'TrendCharts'" />
|
||||
</el-icon>
|
||||
<span :class="metric.trend > 0 ? 'text-green-500' : 'text-red-500'">
|
||||
{{ Math.abs(metric.trend) }}%
|
||||
</span>
|
||||
<span class="text-gray-500 ml-1">vs last period</span>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`p-3 rounded-full ${metric.iconBg}`">
|
||||
<component :is="metric.icon" class="h-6 w-6" :class="metric.iconColor" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- Charts Row 1 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Messages Over Time -->
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>Messages Over Time</span>
|
||||
</template>
|
||||
<div class="chart-container" style="height: 350px">
|
||||
<Line :data="messagesChartData" :options="chartOptions" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- Engagement Rate -->
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>Engagement Rate by Day</span>
|
||||
</template>
|
||||
<div class="chart-container" style="height: 350px">
|
||||
<Bar :data="engagementChartData" :options="barChartOptions" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- Charts Row 2 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Campaign Performance -->
|
||||
<el-card class="lg:col-span-2">
|
||||
<template #header>
|
||||
<span>Campaign Performance Comparison</span>
|
||||
</template>
|
||||
<el-table :data="campaignPerformance" style="width: 100%">
|
||||
<el-table-column prop="name" label="Campaign" min-width="200" />
|
||||
<el-table-column prop="messages" label="Messages" width="100" />
|
||||
<el-table-column prop="delivered" label="Delivered" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.delivered }} ({{ row.deliveryRate }}%)
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="opened" label="Opened" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.opened }} ({{ row.openRate }}%)
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="clicked" label="Clicked" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.clicked }} ({{ row.ctr }}%)
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="conversions" label="Conversions" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.conversions }} ({{ row.conversionRate }}%)
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- Top Performing Content -->
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>Top Performing Content</span>
|
||||
</template>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="(content, index) in topContent"
|
||||
:key="index"
|
||||
class="p-3 bg-gray-50 rounded-lg"
|
||||
>
|
||||
<p class="text-sm font-medium text-gray-900 truncate">{{ content.preview }}</p>
|
||||
<div class="mt-1 flex items-center justify-between text-xs text-gray-500">
|
||||
<span>{{ content.engagement }}% engagement</span>
|
||||
<span>{{ content.clicks }} clicks</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- Real-time Activity -->
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Real-time Activity</span>
|
||||
<el-badge :value="realtimeActivity.length" class="item" type="primary">
|
||||
<el-button size="small" :icon="realtimeEnabled ? 'VideoPause' : 'VideoPlay'" @click="toggleRealtime">
|
||||
{{ realtimeEnabled ? 'Pause' : 'Resume' }}
|
||||
</el-button>
|
||||
</el-badge>
|
||||
</div>
|
||||
</template>
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="activity in realtimeActivity"
|
||||
:key="activity.id"
|
||||
:timestamp="activity.timestamp"
|
||||
:type="activity.type"
|
||||
>
|
||||
{{ activity.description }}
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Line, Bar } from 'vue-chartjs'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
} from 'chart.js'
|
||||
import {
|
||||
Message,
|
||||
TrendCharts,
|
||||
SuccessFilled,
|
||||
User
|
||||
} from '@element-plus/icons-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import api from '@/api'
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
BarElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const dateRange = ref([
|
||||
dayjs().subtract(7, 'days').toDate(),
|
||||
dayjs().toDate()
|
||||
])
|
||||
|
||||
const realtimeEnabled = ref(true)
|
||||
const realtimeActivity = ref([])
|
||||
let realtimeInterval = null
|
||||
|
||||
const keyMetrics = ref([
|
||||
{
|
||||
title: 'Total Messages',
|
||||
value: '156,234',
|
||||
trend: 12.5,
|
||||
icon: Message,
|
||||
iconBg: 'bg-blue-100',
|
||||
iconColor: 'text-blue-600'
|
||||
},
|
||||
{
|
||||
title: 'Engagement Rate',
|
||||
value: '68.4%',
|
||||
trend: 5.2,
|
||||
icon: TrendCharts,
|
||||
iconBg: 'bg-green-100',
|
||||
iconColor: 'text-green-600'
|
||||
},
|
||||
{
|
||||
title: 'Conversion Rate',
|
||||
value: '12.8%',
|
||||
trend: -2.1,
|
||||
icon: SuccessFilled,
|
||||
iconBg: 'bg-purple-100',
|
||||
iconColor: 'text-purple-600'
|
||||
},
|
||||
{
|
||||
title: 'Active Users',
|
||||
value: '8,432',
|
||||
trend: 8.7,
|
||||
icon: User,
|
||||
iconBg: 'bg-orange-100',
|
||||
iconColor: 'text-orange-600'
|
||||
}
|
||||
])
|
||||
|
||||
const messagesChartData = reactive({
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Messages Sent',
|
||||
data: [],
|
||||
borderColor: '#06b6d4',
|
||||
backgroundColor: 'rgba(6, 182, 212, 0.1)',
|
||||
tension: 0.4
|
||||
},
|
||||
{
|
||||
label: 'Messages Delivered',
|
||||
data: [],
|
||||
borderColor: '#10b981',
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||||
tension: 0.4
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const engagementChartData = reactive({
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Open Rate',
|
||||
data: [],
|
||||
backgroundColor: '#8b5cf6'
|
||||
},
|
||||
{
|
||||
label: 'Click Rate',
|
||||
data: [],
|
||||
backgroundColor: '#f59e0b'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const barChartOptions = {
|
||||
...chartOptions,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return value + '%'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const campaignPerformance = ref([])
|
||||
|
||||
const topContent = ref([
|
||||
{
|
||||
preview: '🎉 Special offer just for you! Get 20% off...',
|
||||
engagement: 78,
|
||||
clicks: 1234
|
||||
},
|
||||
{
|
||||
preview: 'Don\'t miss out on our exclusive deals...',
|
||||
engagement: 65,
|
||||
clicks: 987
|
||||
},
|
||||
{
|
||||
preview: 'New products are here! Check them out...',
|
||||
engagement: 62,
|
||||
clicks: 856
|
||||
},
|
||||
{
|
||||
preview: 'Last chance to save big on your favorites...',
|
||||
engagement: 58,
|
||||
clicks: 745
|
||||
}
|
||||
])
|
||||
|
||||
const loadAnalytics = async () => {
|
||||
try {
|
||||
const params = {
|
||||
startDate: dayjs(dateRange.value[0]).format('YYYY-MM-DD'),
|
||||
endDate: dayjs(dateRange.value[1]).format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
// Load message metrics
|
||||
const metricsResponse = await api.analytics.getMessageMetrics(params)
|
||||
updateChartData(metricsResponse.data)
|
||||
|
||||
// Load campaign performance
|
||||
const campaignsResponse = await api.analytics.getCampaignMetrics(null, params)
|
||||
campaignPerformance.value = campaignsResponse.data.campaigns
|
||||
|
||||
// Load engagement metrics
|
||||
const engagementResponse = await api.analytics.getEngagementMetrics(params)
|
||||
updateEngagementChart(engagementResponse.data)
|
||||
} catch (error) {
|
||||
console.error('Failed to load analytics:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const updateChartData = (data) => {
|
||||
messagesChartData.labels = data.timeline.map(item =>
|
||||
dayjs(item.date).format('MM/DD')
|
||||
)
|
||||
messagesChartData.datasets[0].data = data.timeline.map(item => item.sent)
|
||||
messagesChartData.datasets[1].data = data.timeline.map(item => item.delivered)
|
||||
}
|
||||
|
||||
const updateEngagementChart = (data) => {
|
||||
engagementChartData.labels = data.daily.map(item =>
|
||||
dayjs(item.date).format('MM/DD')
|
||||
)
|
||||
engagementChartData.datasets[0].data = data.daily.map(item => item.openRate)
|
||||
engagementChartData.datasets[1].data = data.daily.map(item => item.clickRate)
|
||||
}
|
||||
|
||||
const loadRealtimeActivity = async () => {
|
||||
if (!realtimeEnabled.value) return
|
||||
|
||||
try {
|
||||
const response = await api.analytics.getRealTimeMetrics()
|
||||
|
||||
// Add new activities to the beginning
|
||||
const newActivities = response.data.activities.map(activity => ({
|
||||
id: Date.now() + Math.random(),
|
||||
timestamp: dayjs(activity.timestamp).format('HH:mm:ss'),
|
||||
description: activity.description,
|
||||
type: activity.type || 'primary'
|
||||
}))
|
||||
|
||||
realtimeActivity.value = [...newActivities, ...realtimeActivity.value].slice(0, 10)
|
||||
} catch (error) {
|
||||
console.error('Failed to load realtime activity:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleRealtime = () => {
|
||||
realtimeEnabled.value = !realtimeEnabled.value
|
||||
if (realtimeEnabled.value) {
|
||||
loadRealtimeActivity()
|
||||
}
|
||||
}
|
||||
|
||||
const exportReport = async () => {
|
||||
try {
|
||||
const response = await api.analytics.generateReport({
|
||||
type: 'comprehensive',
|
||||
startDate: dayjs(dateRange.value[0]).format('YYYY-MM-DD'),
|
||||
endDate: dayjs(dateRange.value[1]).format('YYYY-MM-DD'),
|
||||
format: 'pdf'
|
||||
})
|
||||
|
||||
ElMessage.success('Report generation started. You will be notified when it\'s ready.')
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to generate report')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAnalytics()
|
||||
loadRealtimeActivity()
|
||||
|
||||
// Set up realtime updates
|
||||
realtimeInterval = setInterval(() => {
|
||||
loadRealtimeActivity()
|
||||
}, 5000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (realtimeInterval) {
|
||||
clearInterval(realtimeInterval)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
184
marketing-agent/frontend/src/views/Compliance.vue
Normal file
184
marketing-agent/frontend/src/views/Compliance.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div class="compliance-page">
|
||||
<el-card class="page-header">
|
||||
<h1>{{ $t('compliance.title') }}</h1>
|
||||
<p>{{ $t('compliance.subtitle') }}</p>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<!-- Compliance Status -->
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>{{ $t('compliance.status') }}</span>
|
||||
</template>
|
||||
<div class="compliance-status">
|
||||
<div class="status-item">
|
||||
<span>GDPR Compliance</span>
|
||||
<el-tag type="success">Compliant</el-tag>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>CCPA Compliance</span>
|
||||
<el-tag type="success">Compliant</el-tag>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>Data Retention Policy</span>
|
||||
<el-tag type="success">Active</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- Data Requests -->
|
||||
<el-col :span="12">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>{{ $t('compliance.dataRequests') }}</span>
|
||||
</template>
|
||||
<el-table :data="dataRequests" style="width: 100%">
|
||||
<el-table-column prop="type" :label="$t('compliance.requestType')" />
|
||||
<el-table-column prop="status" :label="$t('compliance.status')">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getRequestStatusType(row.status)">
|
||||
{{ row.status }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" :label="$t('compliance.date')" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- Audit Log -->
|
||||
<el-row :gutter="20" class="mt-4">
|
||||
<el-col :span="24">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('compliance.auditLog') }}</span>
|
||||
<el-button @click="exportAuditLog">
|
||||
{{ $t('compliance.exportLog') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="auditLogs" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="action" :label="$t('compliance.action')" />
|
||||
<el-table-column prop="user" :label="$t('compliance.user')" />
|
||||
<el-table-column prop="timestamp" :label="$t('compliance.timestamp')" />
|
||||
<el-table-column prop="details" :label="$t('compliance.details')">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="viewDetails(row)">
|
||||
{{ $t('common.view') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import api from '@/api'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataRequests = ref([])
|
||||
const auditLogs = ref([])
|
||||
|
||||
const loadDataRequests = async () => {
|
||||
try {
|
||||
const response = await api.compliance.getDataRequests()
|
||||
dataRequests.value = response.data
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to load data requests')
|
||||
}
|
||||
}
|
||||
|
||||
const loadAuditLogs = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await api.compliance.getAuditLogs()
|
||||
auditLogs.value = response.data
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to load audit logs')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getRequestStatusType = (status) => {
|
||||
const types = {
|
||||
pending: 'warning',
|
||||
processing: 'primary',
|
||||
completed: 'success',
|
||||
failed: 'danger'
|
||||
}
|
||||
return types[status] || 'info'
|
||||
}
|
||||
|
||||
const viewDetails = (log) => {
|
||||
// Show log details
|
||||
}
|
||||
|
||||
const exportAuditLog = async () => {
|
||||
try {
|
||||
await api.compliance.exportAuditLog()
|
||||
ElMessage.success('Audit log exported successfully')
|
||||
} catch (error) {
|
||||
ElMessage.error('Failed to export audit log')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDataRequests()
|
||||
loadAuditLogs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.compliance-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h1 {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.compliance-status {
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
297
marketing-agent/frontend/src/views/Dashboard.vue
Normal file
297
marketing-agent/frontend/src/views/Dashboard.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<!-- Mobile Dashboard -->
|
||||
<DashboardMobile v-if="isMobile" />
|
||||
|
||||
<!-- Desktop Dashboard -->
|
||||
<div v-else class="space-y-6">
|
||||
<!-- Overview Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<el-card v-for="stat in stats" :key="stat.title" shadow="hover">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">{{ t(stat.title) }}</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-gray-900">
|
||||
{{ stat.value }}
|
||||
</p>
|
||||
<p class="mt-1 text-sm" :class="stat.changeClass">
|
||||
{{ stat.change }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3 rounded-full" :class="stat.iconBg">
|
||||
<component :is="stat.icon" class="h-6 w-6" :class="stat.iconColor" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- Charts Row -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Campaign Performance Chart -->
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Campaign Performance</span>
|
||||
<el-select v-model="timeRange" size="small" style="width: 120px">
|
||||
<el-option label="Last 7 days" value="7d" />
|
||||
<el-option label="Last 30 days" value="30d" />
|
||||
<el-option label="Last 90 days" value="90d" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
<div class="chart-container" style="height: 300px">
|
||||
<Line :data="campaignChartData" :options="chartOptions" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- Engagement Distribution -->
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>Engagement Distribution</span>
|
||||
</template>
|
||||
<div class="chart-container" style="height: 300px">
|
||||
<Doughnut :data="engagementChartData" :options="doughnutOptions" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity & Quick Actions -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Recent Activity -->
|
||||
<el-card class="lg:col-span-2">
|
||||
<template #header>
|
||||
<span>{{ t('dashboard.recentActivity') }}</span>
|
||||
</template>
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="activity in recentActivities"
|
||||
:key="activity.id"
|
||||
:timestamp="activity.timestamp"
|
||||
:type="activity.type"
|
||||
>
|
||||
{{ activity.content }}
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>{{ t('dashboard.quickActions') }}</span>
|
||||
</template>
|
||||
<div class="space-y-3">
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="Plus"
|
||||
class="w-full"
|
||||
@click="$router.push('/campaigns/create')"
|
||||
>
|
||||
{{ t('dashboard.createCampaign') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
icon="DataAnalysis"
|
||||
class="w-full"
|
||||
@click="$router.push('/analytics')"
|
||||
>
|
||||
{{ t('dashboard.viewAnalytics') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
icon="User"
|
||||
class="w-full"
|
||||
@click="$router.push('/accounts')"
|
||||
>
|
||||
{{ t('dashboard.manageAccounts') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Line, Doughnut } from 'vue-chartjs'
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ArcElement
|
||||
} from 'chart.js'
|
||||
import {
|
||||
Management,
|
||||
Message,
|
||||
TrendCharts,
|
||||
SuccessFilled
|
||||
} from '@element-plus/icons-vue'
|
||||
import api from '@/api'
|
||||
import DashboardMobile from './DashboardMobile.vue'
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ArcElement
|
||||
)
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const timeRange = ref('7d')
|
||||
const isMobile = ref(false)
|
||||
|
||||
// Check if device is mobile
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth < 768
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', checkMobile)
|
||||
})
|
||||
|
||||
const stats = reactive([
|
||||
{
|
||||
title: 'dashboard.activeCampaigns',
|
||||
value: '12',
|
||||
change: '+2 from last week',
|
||||
changeClass: 'text-green-600',
|
||||
icon: Management,
|
||||
iconBg: 'bg-blue-100',
|
||||
iconColor: 'text-blue-600'
|
||||
},
|
||||
{
|
||||
title: 'dashboard.totalMessages',
|
||||
value: '24,563',
|
||||
change: '+15% from last week',
|
||||
changeClass: 'text-green-600',
|
||||
icon: Message,
|
||||
iconBg: 'bg-green-100',
|
||||
iconColor: 'text-green-600'
|
||||
},
|
||||
{
|
||||
title: 'dashboard.engagementRate',
|
||||
value: '68.4%',
|
||||
change: '+5.2% from last week',
|
||||
changeClass: 'text-green-600',
|
||||
icon: TrendCharts,
|
||||
iconBg: 'bg-purple-100',
|
||||
iconColor: 'text-purple-600'
|
||||
},
|
||||
{
|
||||
title: 'dashboard.conversionRate',
|
||||
value: '12.8%',
|
||||
change: '-2.1% from last week',
|
||||
changeClass: 'text-red-600',
|
||||
icon: SuccessFilled,
|
||||
iconBg: 'bg-yellow-100',
|
||||
iconColor: 'text-yellow-600'
|
||||
}
|
||||
])
|
||||
|
||||
const campaignChartData = reactive({
|
||||
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Messages Sent',
|
||||
data: [3200, 4100, 3800, 4500, 4200, 5100, 4800],
|
||||
borderColor: '#06b6d4',
|
||||
backgroundColor: 'rgba(6, 182, 212, 0.1)',
|
||||
tension: 0.4
|
||||
},
|
||||
{
|
||||
label: 'Engagement',
|
||||
data: [2100, 2800, 2600, 3200, 2900, 3500, 3300],
|
||||
borderColor: '#8b5cf6',
|
||||
backgroundColor: 'rgba(139, 92, 246, 0.1)',
|
||||
tension: 0.4
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const engagementChartData = reactive({
|
||||
labels: ['Opened', 'Clicked', 'Converted', 'Ignored'],
|
||||
datasets: [
|
||||
{
|
||||
data: [45, 23, 12, 20],
|
||||
backgroundColor: ['#06b6d4', '#8b5cf6', '#10b981', '#ef4444'],
|
||||
borderWidth: 0
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const doughnutOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const recentActivities = ref([
|
||||
{
|
||||
id: 1,
|
||||
content: 'Campaign "Summer Sale" completed successfully',
|
||||
timestamp: '2024-01-20 14:30',
|
||||
type: 'success'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: 'New A/B test "Welcome Message" started',
|
||||
timestamp: '2024-01-20 13:15',
|
||||
type: 'primary'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: '500 new contacts imported',
|
||||
timestamp: '2024-01-20 11:45',
|
||||
type: 'info'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
content: 'Campaign "Flash Sale" paused due to rate limit',
|
||||
timestamp: '2024-01-20 10:20',
|
||||
type: 'warning'
|
||||
}
|
||||
])
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Load dashboard metrics
|
||||
const response = await api.analytics.getDashboardMetrics({
|
||||
timeRange: timeRange.value
|
||||
})
|
||||
// Update stats and charts with real data
|
||||
} catch (error) {
|
||||
console.error('Failed to load dashboard metrics:', error)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
210
marketing-agent/frontend/src/views/DashboardEnhanced.vue
Normal file
210
marketing-agent/frontend/src/views/DashboardEnhanced.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- Overview Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<el-card v-for="stat in stats" :key="stat.title" shadow="hover">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">{{ stat.title }}</p>
|
||||
<p class="mt-2 text-3xl font-semibold text-gray-900">
|
||||
{{ stat.value }}
|
||||
</p>
|
||||
<p class="mt-1 text-sm" :class="stat.change > 0 ? 'text-green-600' : 'text-red-600'">
|
||||
{{ stat.change > 0 ? '+' : '' }}{{ stat.change }}%
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3 rounded-full bg-blue-100">
|
||||
<el-icon class="text-2xl text-blue-600">
|
||||
<DataAnalysis />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- Charts Row -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Campaign Performance Chart -->
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Campaign Performance</span>
|
||||
<el-select v-model="timeRange" size="small" style="width: 120px">
|
||||
<el-option label="Last 7 days" value="7d" />
|
||||
<el-option label="Last 30 days" value="30d" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="chartsSupported && campaignChartData" class="chart-container" style="height: 300px">
|
||||
<LineChart :data="campaignChartData" :options="chartOptions" />
|
||||
</div>
|
||||
<div v-else class="h-[300px] flex items-center justify-center text-gray-500">
|
||||
<p>Chart visualization not available</p>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>Recent Activity</span>
|
||||
</template>
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="activity in recentActivities"
|
||||
:key="activity.id"
|
||||
:timestamp="formatTime(activity.timestamp)"
|
||||
:type="getActivityType(activity.type)"
|
||||
>
|
||||
{{ activity.content || activity.campaign }}
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>Quick Actions</span>
|
||||
</template>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<el-button type="primary" @click="$router.push('/campaigns/create')">
|
||||
<el-icon class="mr-2"><Plus /></el-icon>
|
||||
New Campaign
|
||||
</el-button>
|
||||
<el-button @click="$router.push('/analytics')">
|
||||
<el-icon class="mr-2"><DataAnalysis /></el-icon>
|
||||
View Analytics
|
||||
</el-button>
|
||||
<el-button @click="$router.push('/accounts')">
|
||||
<el-icon class="mr-2"><User /></el-icon>
|
||||
Manage Accounts
|
||||
</el-button>
|
||||
<el-button @click="$router.push('/settings')">
|
||||
<el-icon class="mr-2"><Setting /></el-icon>
|
||||
Settings
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, shallowRef } from 'vue'
|
||||
import { DataAnalysis, Plus, User, Setting } from '@element-plus/icons-vue'
|
||||
import api from '@/api'
|
||||
|
||||
// Chart components - lazy loaded
|
||||
let LineChart = null
|
||||
let chartsSupported = ref(false)
|
||||
|
||||
// Try to load chart components
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const chartModule = await import('vue-chartjs')
|
||||
const chartjsModule = await import('chart.js')
|
||||
|
||||
// Register Chart.js components
|
||||
const { Chart, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend } = chartjsModule
|
||||
Chart.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend)
|
||||
|
||||
// Set Line chart component
|
||||
LineChart = chartModule.Line
|
||||
chartsSupported.value = true
|
||||
} catch (error) {
|
||||
console.warn('Charts not available:', error)
|
||||
chartsSupported.value = false
|
||||
}
|
||||
|
||||
// Load dashboard data
|
||||
loadDashboardData()
|
||||
})
|
||||
|
||||
const timeRange = ref('7d')
|
||||
|
||||
const stats = ref([
|
||||
{ title: 'Active Campaigns', value: 0, change: 0 },
|
||||
{ title: 'Total Messages', value: 0, change: 0 },
|
||||
{ title: 'Delivery Rate', value: '0%', change: 0 },
|
||||
{ title: 'Click Rate', value: '0%', change: 0 }
|
||||
])
|
||||
|
||||
const campaignChartData = ref(null)
|
||||
const recentActivities = ref([])
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: 'bottom' }
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
if (!timestamp) return ''
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
const getActivityType = (type) => {
|
||||
const typeMap = {
|
||||
'campaign_started': 'success',
|
||||
'message_sent': 'primary',
|
||||
'campaign_completed': 'info'
|
||||
}
|
||||
return typeMap[type] || 'info'
|
||||
}
|
||||
|
||||
const loadDashboardData = async () => {
|
||||
try {
|
||||
const response = await api.analytics.getDashboardMetrics({ timeRange: timeRange.value })
|
||||
|
||||
if (response.data?.data) {
|
||||
const { overview, recentActivity, performance } = response.data.data
|
||||
|
||||
// Update stats
|
||||
if (overview) {
|
||||
stats.value = [
|
||||
{ title: 'Active Campaigns', value: overview.activeCampaigns || 0, change: 2 },
|
||||
{ title: 'Total Messages', value: overview.totalMessages || 0, change: 15 },
|
||||
{ title: 'Delivery Rate', value: `${overview.deliveryRate || 0}%`, change: 0.3 },
|
||||
{ title: 'Click Rate', value: `${overview.clickRate || 0}%`, change: -1.2 }
|
||||
]
|
||||
}
|
||||
|
||||
// Update activities
|
||||
recentActivities.value = recentActivity || []
|
||||
|
||||
// Update chart data if charts are supported
|
||||
if (chartsSupported.value && performance?.daily) {
|
||||
campaignChartData.value = {
|
||||
labels: performance.daily.map(d => d.date.split('-').pop()),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Messages Sent',
|
||||
data: performance.daily.map(d => d.sent),
|
||||
borderColor: '#06b6d4',
|
||||
backgroundColor: 'rgba(6, 182, 212, 0.1)'
|
||||
},
|
||||
{
|
||||
label: 'Messages Delivered',
|
||||
data: performance.daily.map(d => d.delivered),
|
||||
borderColor: '#8b5cf6',
|
||||
backgroundColor: 'rgba(139, 92, 246, 0.1)'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load dashboard data:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
261
marketing-agent/frontend/src/views/DashboardMobile.vue
Normal file
261
marketing-agent/frontend/src/views/DashboardMobile.vue
Normal file
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<div class="p-4 pb-20">
|
||||
<!-- Quick Stats -->
|
||||
<div class="grid grid-cols-2 gap-3 mb-6">
|
||||
<el-card
|
||||
v-for="stat in stats"
|
||||
:key="stat.title"
|
||||
:body-style="{ padding: '12px' }"
|
||||
class="text-center"
|
||||
>
|
||||
<div class="text-2xl font-bold" :style="{ color: stat.color }">
|
||||
{{ stat.value }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 mt-1">{{ stat.title }}</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- Campaign Performance Chart -->
|
||||
<el-card class="mb-4" :body-style="{ padding: '12px' }">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium">{{ t('dashboard.campaignPerformance') }}</span>
|
||||
<el-button text size="small" @click="refreshData">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="h-48">
|
||||
<LineChart :data="campaignData" :options="mobileChartOptions" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- Recent Campaigns -->
|
||||
<el-card :body-style="{ padding: '12px' }">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm font-medium">{{ t('dashboard.recentCampaigns') }}</span>
|
||||
<router-link to="/campaigns" class="text-blue-600 text-xs">
|
||||
{{ t('common.viewAll') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="campaign in recentCampaigns"
|
||||
:key="campaign.id"
|
||||
class="border-b border-gray-100 pb-3 last:border-0"
|
||||
>
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h4 class="text-sm font-medium text-gray-900 truncate">
|
||||
{{ campaign.name }}
|
||||
</h4>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
{{ formatDate(campaign.createdAt) }}
|
||||
</p>
|
||||
</div>
|
||||
<el-tag
|
||||
:type="getStatusType(campaign.status)"
|
||||
size="small"
|
||||
effect="plain"
|
||||
>
|
||||
{{ campaign.status }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-2 text-xs">
|
||||
<span class="text-gray-600">
|
||||
<el-icon class="align-middle"><User /></el-icon>
|
||||
{{ campaign.recipients }}
|
||||
</span>
|
||||
<span class="text-gray-600">
|
||||
<el-icon class="align-middle"><View /></el-icon>
|
||||
{{ campaign.delivered }}
|
||||
</span>
|
||||
<span class="text-gray-600">
|
||||
<el-icon class="align-middle"><ChatDotRound /></el-icon>
|
||||
{{ campaign.engagement }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="fixed bottom-20 right-4 z-10">
|
||||
<el-dropdown @command="handleQuickAction" placement="top">
|
||||
<el-button type="primary" circle size="large">
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="campaign">
|
||||
<el-icon><Management /></el-icon>
|
||||
{{ t('dashboard.createCampaign') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="template">
|
||||
<el-icon><DocumentCopy /></el-icon>
|
||||
{{ t('dashboard.createTemplate') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="segment">
|
||||
<el-icon><User /></el-icon>
|
||||
{{ t('dashboard.createSegment') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
Refresh,
|
||||
User,
|
||||
View,
|
||||
ChatDotRound,
|
||||
Plus,
|
||||
Management,
|
||||
DocumentCopy
|
||||
} from '@element-plus/icons-vue'
|
||||
import LineChart from '@/components/charts/LineChart.vue'
|
||||
import { formatDate } from '@/utils/date'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
// Stats data
|
||||
const stats = ref([
|
||||
{ title: t('dashboard.totalCampaigns'), value: '156', color: '#3b82f6' },
|
||||
{ title: t('dashboard.activeUsers'), value: '2.4K', color: '#10b981' },
|
||||
{ title: t('dashboard.messagesSent'), value: '45.2K', color: '#f59e0b' },
|
||||
{ title: t('dashboard.avgEngagement'), value: '68%', color: '#8b5cf6' }
|
||||
])
|
||||
|
||||
// Campaign performance data
|
||||
const campaignData = ref({
|
||||
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Messages Sent',
|
||||
data: [1200, 1900, 1500, 2100, 2400, 2200, 2800],
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
tension: 0.4
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Mobile-optimized chart options
|
||||
const mobileChartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 10
|
||||
}
|
||||
}
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
borderDash: [3, 3]
|
||||
},
|
||||
ticks: {
|
||||
font: {
|
||||
size: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recent campaigns
|
||||
const recentCampaigns = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Summer Sale Promotion',
|
||||
status: 'active',
|
||||
createdAt: new Date('2024-01-15'),
|
||||
recipients: 1500,
|
||||
delivered: 1423,
|
||||
engagement: 72
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'New Product Launch',
|
||||
status: 'scheduled',
|
||||
createdAt: new Date('2024-01-14'),
|
||||
recipients: 2000,
|
||||
delivered: 0,
|
||||
engagement: 0
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Customer Survey',
|
||||
status: 'completed',
|
||||
createdAt: new Date('2024-01-13'),
|
||||
recipients: 500,
|
||||
delivered: 487,
|
||||
engagement: 45
|
||||
}
|
||||
])
|
||||
|
||||
const getStatusType = (status) => {
|
||||
const types = {
|
||||
active: 'success',
|
||||
scheduled: 'info',
|
||||
completed: 'info',
|
||||
paused: 'warning',
|
||||
failed: 'danger'
|
||||
}
|
||||
return types[status] || 'info'
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
ElMessage.success(t('common.refreshSuccess'))
|
||||
}
|
||||
|
||||
const handleQuickAction = (command) => {
|
||||
switch (command) {
|
||||
case 'campaign':
|
||||
router.push('/campaigns/create')
|
||||
break
|
||||
case 'template':
|
||||
router.push('/templates/create')
|
||||
break
|
||||
case 'segment':
|
||||
router.push('/users/segments/create')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Load dashboard data
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-card {
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
||||
85
marketing-agent/frontend/src/views/DashboardSimple.vue
Normal file
85
marketing-agent/frontend/src/views/DashboardSimple.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<h1 class="text-2xl font-bold mb-4">Dashboard</h1>
|
||||
|
||||
<!-- Simple Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-white p-4 rounded shadow">
|
||||
<h3 class="text-sm text-gray-500">Active Campaigns</h3>
|
||||
<p class="text-2xl font-bold">{{ stats.activeCampaigns }}</p>
|
||||
</div>
|
||||
<div class="bg-white p-4 rounded shadow">
|
||||
<h3 class="text-sm text-gray-500">Total Messages</h3>
|
||||
<p class="text-2xl font-bold">{{ stats.totalMessages }}</p>
|
||||
</div>
|
||||
<div class="bg-white p-4 rounded shadow">
|
||||
<h3 class="text-sm text-gray-500">Delivery Rate</h3>
|
||||
<p class="text-2xl font-bold">{{ stats.deliveryRate }}%</p>
|
||||
</div>
|
||||
<div class="bg-white p-4 rounded shadow">
|
||||
<h3 class="text-sm text-gray-500">Click Rate</h3>
|
||||
<p class="text-2xl font-bold">{{ stats.clickRate }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="bg-white p-6 rounded shadow">
|
||||
<h2 class="text-lg font-bold mb-4">Recent Activity</h2>
|
||||
<div class="space-y-2">
|
||||
<div v-for="activity in recentActivity" :key="activity.id" class="border-l-4 border-blue-500 pl-4 py-2">
|
||||
<p class="font-medium">{{ activity.content }}</p>
|
||||
<p class="text-sm text-gray-500">{{ activity.timestamp }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import api from '@/api'
|
||||
|
||||
const stats = ref({
|
||||
activeCampaigns: 0,
|
||||
totalMessages: 0,
|
||||
deliveryRate: 0,
|
||||
clickRate: 0
|
||||
})
|
||||
|
||||
const recentActivity = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// Load dashboard data
|
||||
const response = await api.analytics.getDashboardMetrics()
|
||||
console.log('Dashboard data:', response.data)
|
||||
|
||||
if (response.data && response.data.data) {
|
||||
const data = response.data.data
|
||||
stats.value = {
|
||||
activeCampaigns: data.overview?.activeCampaigns || 0,
|
||||
totalMessages: data.overview?.totalMessages || 0,
|
||||
deliveryRate: data.overview?.deliveryRate || 0,
|
||||
clickRate: data.overview?.clickRate || 0
|
||||
}
|
||||
recentActivity.value = data.recentActivity || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load dashboard:', error)
|
||||
// Use default data
|
||||
stats.value = {
|
||||
activeCampaigns: 5,
|
||||
totalMessages: 45678,
|
||||
deliveryRate: 98.5,
|
||||
clickRate: 12.3
|
||||
}
|
||||
recentActivity.value = [
|
||||
{
|
||||
id: 1,
|
||||
content: 'Campaign "Summer Sale" started',
|
||||
timestamp: new Date().toLocaleString()
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user