Initial commit: Telegram Management System
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:
你的用户名
2025-11-04 15:37:50 +08:00
commit 237c7802e5
3674 changed files with 525172 additions and 0 deletions

View 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

View 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

View 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

View 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
View 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
View 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:'

View 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']
});
}

View 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
View 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

View 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. **自动化催收**:智能催款流程

View 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

View 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

View 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
View 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.

View 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();

View 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

View 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

View 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.

View 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)

View 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

View 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

View 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

View 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.

View 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.

View 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}')
```

View 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 |

View File

@@ -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}`);",
"}"
]
}
}
]
}

View 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

View 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

View 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View 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
View 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

View 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;"]

View 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.

View 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

View 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.

View 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>

View 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;
}
}

View 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"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View 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>

View 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>

View 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))
)
})
)
}
})

View 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>

View 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>

View 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>

View 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'
})
}

View 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

View 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
})
}
}

View 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 })
}
}

View 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}`)
}
}

View 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 })
}
}

View 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}`)
}
}

View 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 })
}
}

View 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
})
}
}

View File

@@ -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}`)
}
}

View 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')
}
}

View 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'
})
}
}

View 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')
}
}

View 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'
})
}
}

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View 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
}
}

View 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
}
}

View 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
}
}

View 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"
}
}

View 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

View 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": "不合规"
}
}

View 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')

View 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
}

View 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'
}
}

View 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

View 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
}
})

View 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
}
})

View 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)
})
}
}

View 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;
}

View 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));
}
}

View 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
}
}

View 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()

View 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)

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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