diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index f93b73f3..aba4814b 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,322 +1,381 @@ # SmoothSchedule Production Deployment Guide +This guide covers deploying SmoothSchedule to a production server. + +## Table of Contents + +1. [Prerequisites](#prerequisites) +2. [Quick Reference](#quick-reference) +3. [Initial Server Setup](#initial-server-setup-first-time-only) +4. [Regular Deployments](#regular-deployments) +5. [Activepieces Updates](#activepieces-updates) +6. [Troubleshooting](#troubleshooting) +7. [Maintenance](#maintenance) + ## Prerequisites ### Server Requirements -- Ubuntu/Debian Linux server -- Minimum 2GB RAM, 20GB disk space -- Docker and Docker Compose installed -- Domain name pointed to server IP: `smoothschedule.com` -- DNS configured with wildcard subdomain: `*.smoothschedule.com` +- Ubuntu 20.04+ or Debian 11+ +- 4GB RAM minimum (2GB works but cannot build Activepieces image) +- 40GB disk space +- Docker and Docker Compose v2 installed +- Domain with wildcard DNS configured + +### Local Requirements (for deployment) +- Git access to the repository +- SSH access to the production server +- Docker (for building Activepieces image) ### Required Accounts/Services -- [x] DigitalOcean Spaces (already configured) - - Access Key: DO801P4R8QXYMY4CE8WZ - - Bucket: smoothschedule - - Region: nyc3 -- [ ] Email service (optional - Mailgun or SMTP) -- [ ] Sentry (optional - error tracking) +- DigitalOcean Spaces (for static/media files) +- Stripe (for payments) +- Twilio (for SMS/phone features) +- OpenAI API (optional, for Activepieces AI copilot) -## Pre-Deployment Checklist - -### 1. DigitalOcean Spaces Setup +## Quick Reference ```bash -# Create the bucket (if not already created) -aws --profile do-tor1 s3 mb s3://smoothschedule +# Regular deployment (after initial setup) +./deploy.sh -# Set bucket to public-read for static/media files -aws --profile do-tor1 s3api put-bucket-acl \ - --bucket smoothschedule \ - --acl public-read +# Deploy with Activepieces image rebuild +./deploy.sh --deploy-ap -# Configure CORS (for frontend uploads) -cat > cors.json < . -git checkout main - -# Option B: Copy from local machine -# From your local machine: -# rsync -avz --exclude 'node_modules' --exclude '.venv' --exclude '__pycache__' \ -# /home/poduck/Desktop/smoothschedule2/ poduck@smoothschedule.com:~/smoothschedule/ - -# Navigate to backend -cd smoothschedule - -# Build and start containers -docker compose -f docker-compose.production.yml build -docker compose -f docker-compose.production.yml up -d - -# Wait for containers to start -sleep 10 - -# Check logs -docker compose -f docker-compose.production.yml logs -f +./scripts/build-activepieces.sh deploy ``` -### Step 3: Database Initialization +Or manually: ```bash -# Run migrations -docker compose -f docker-compose.production.yml exec django python manage.py migrate - -# Create public schema (for multi-tenancy) -docker compose -f docker-compose.production.yml exec django python manage.py migrate_schemas --shared - -# Create superuser -docker compose -f docker-compose.production.yml exec django python manage.py createsuperuser - -# Collect static files (uploads to DigitalOcean Spaces) -docker compose -f docker-compose.production.yml exec django python manage.py collectstatic --noinput +cd activepieces-fork +docker build -t smoothschedule_production_activepieces . +docker save smoothschedule_production_activepieces | gzip > /tmp/ap.tar.gz +scp /tmp/ap.tar.gz your-user@your-server:/tmp/ +ssh your-user@your-server 'gunzip -c /tmp/ap.tar.gz | docker load' ``` -### Step 4: Create Initial Tenant +### 6. Run Initialization Script + +```bash +# On the server +cd ~/smoothschedule/smoothschedule +chmod +x scripts/init-production.sh +./scripts/init-production.sh +``` + +This script will: +1. Verify environment files +2. Generate any missing security keys +3. Start PostgreSQL and Redis +4. Create the Activepieces database +5. Start all services +6. Run Django migrations +7. Guide you through Activepieces platform setup + +### 7. Complete Activepieces Platform Setup + +After the init script completes: + +1. Visit `https://automations.yourdomain.com` +2. Create an admin account (this creates the platform) +3. Get the platform ID: + ```bash + docker compose -f docker-compose.production.yml exec postgres \ + psql -U -d activepieces -c "SELECT id FROM platform" + ``` +4. Update `AP_PLATFORM_ID` in both: + - `.envs/.production/.activepieces` + - `.envs/.production/.django` +5. Restart services: + ```bash + docker compose -f docker-compose.production.yml restart + ``` + +### 8. Create First Tenant ```bash -# Access Django shell docker compose -f docker-compose.production.yml exec django python manage.py shell - -# In the shell, create your first business tenant: ``` ```python -from core.models import Business -from django.contrib.auth import get_user_model +from smoothschedule.identity.core.models import Tenant, Domain -User = get_user_model() - -# Create a business -business = Business.objects.create( +# Create tenant +tenant = Tenant.objects.create( name="Demo Business", subdomain="demo", - schema_name="demo", + schema_name="demo" ) -# Verify it was created -print(f"Created business: {business.name} at {business.subdomain}.smoothschedule.com") - -# Create a business owner -owner = User.objects.create_user( - username="demo_owner", - email="owner@demo.com", - password="your_password_here", - role="owner", - business_subdomain="demo" +# Create domain +Domain.objects.create( + tenant=tenant, + domain="demo.yourdomain.com", + is_primary=True ) - -print(f"Created owner: {owner.username}") -exit() ``` -### Step 5: Deploy Frontend +### 9. Provision Activepieces Connection ```bash -# On your local machine -cd /home/poduck/Desktop/smoothschedule2/frontend - -# Install dependencies -npm install - -# Build for production -npm run build - -# Upload build files to server -rsync -avz dist/ poduck@smoothschedule.com:~/smoothschedule-frontend/ - -# On the server, set up nginx or serve via backend +docker compose -f docker-compose.production.yml exec django \ + python manage.py provision_ap_connections --tenant demo ``` -**Option A: Serve via Django (simpler)** - -The Django `collectstatic` command already handles static files. For serving the frontend: - -1. Copy frontend build to Django static folder -2. Django will serve it via Traefik - -**Option B: Separate Nginx (recommended for production)** - -```bash -# Install nginx -sudo apt-get update -sudo apt-get install -y nginx - -# Create nginx config -sudo nano /etc/nginx/sites-available/smoothschedule -``` - -```nginx -server { - listen 80; - server_name smoothschedule.com *.smoothschedule.com; - - # Frontend (React) - location / { - root /home/poduck/smoothschedule-frontend; - try_files $uri $uri/ /index.html; - } - - # Backend API (proxy to Traefik) - location /api { - proxy_pass http://localhost:80; - 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; - } - - location /admin { - proxy_pass http://localhost:80; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } -} -``` - -```bash -# Enable site -sudo ln -s /etc/nginx/sites-available/smoothschedule /etc/nginx/sites-enabled/ -sudo nginx -t -sudo systemctl reload nginx -``` - -### Step 6: SSL/HTTPS Setup - -Traefik is configured to automatically obtain Let's Encrypt SSL certificates. Ensure: - -1. DNS is pointed to your server -2. Ports 80 and 443 are accessible -3. Wait for Traefik to obtain certificates (check logs) - -```bash -# Monitor Traefik logs -docker compose -f docker-compose.production.yml logs -f traefik - -# You should see: -# "Certificate obtained for domain smoothschedule.com" -``` - -### Step 7: Verify Deployment +### 10. Verify Deployment ```bash # Check all containers are running docker compose -f docker-compose.production.yml ps -# Should show: -# - django (running) -# - postgres (running) -# - redis (running) -# - traefik (running) -# - celeryworker (running) -# - celerybeat (running) -# - flower (running) - -# Test API endpoint -curl https://smoothschedule.com/api/ - -# Test admin -curl https://smoothschedule.com/admin/ - -# Access in browser: -# https://smoothschedule.com - Main site -# https://platform.smoothschedule.com - Platform dashboard -# https://demo.smoothschedule.com - Demo business -# https://smoothschedule.com:5555 - Flower (Celery monitoring) +# Test endpoints +curl https://yourdomain.com/api/ +curl https://platform.yourdomain.com/ +curl https://automations.yourdomain.com/api/v1/health ``` -## Post-Deployment +## Regular Deployments -### 1. Monitoring +After initial setup, deployments are simple: ```bash -# View logs -docker compose -f docker-compose.production.yml logs -f +# From your local machine +cd ~/smoothschedule -# View specific service logs -docker compose -f docker-compose.production.yml logs -f django -docker compose -f docker-compose.production.yml logs -f postgres +# Commit and push your changes +git add . +git commit -m "Your changes" +git push -# Monitor Celery tasks via Flower -# Access: https://smoothschedule.com:5555 -# Login with credentials from .envs/.production/.django +# Deploy +./deploy.sh ``` -### 2. Backups +### Deployment Options + +| Command | Description | +|---------|-------------| +| `./deploy.sh` | Full deployment with migrations | +| `./deploy.sh --no-migrate` | Deploy without running migrations | +| `./deploy.sh --deploy-ap` | Rebuild and deploy Activepieces image | +| `./deploy.sh django` | Rebuild only Django container | +| `./deploy.sh nginx traefik` | Rebuild specific services | + +### What the Deploy Script Does + +1. Checks for uncommitted changes +2. Verifies changes are pushed to remote +3. (If `--deploy-ap`) Builds and transfers Activepieces image +4. SSHs to server and pulls latest code +5. Backs up and restores `.envs` directory +6. Builds Docker images +7. Starts containers +8. Sets up Activepieces database (if needed) +9. Runs Django migrations (unless `--no-migrate`) +10. Seeds platform plugins for all tenants + +## Activepieces Updates + +When you modify custom pieces (in `activepieces-fork/`): + +1. Make your changes to piece code +2. Commit and push +3. Deploy with the image flag: + ```bash + ./deploy.sh --deploy-ap + ``` + +The Activepieces container will: +1. Start with the new image +2. Run `publish-pieces.sh` to register custom pieces +3. Insert piece metadata into the database + +### Custom Pieces + +Custom pieces are located in: +- `activepieces-fork/packages/pieces/community/smoothschedule/` - Main SmoothSchedule piece +- `activepieces-fork/packages/pieces/community/python-code/` - Python code execution +- `activepieces-fork/packages/pieces/community/ruby-code/` - Ruby code execution + +Piece metadata is registered via: +- `activepieces-fork/custom-pieces-metadata.sql` - Database registration +- `activepieces-fork/publish-pieces.sh` - Container startup script + +## Troubleshooting + +### View Logs + +```bash +# All services +docker compose -f docker-compose.production.yml logs -f + +# Specific service +docker compose -f docker-compose.production.yml logs -f django +docker compose -f docker-compose.production.yml logs -f activepieces +docker compose -f docker-compose.production.yml logs -f traefik +``` + +### Restart Services + +```bash +# All services +docker compose -f docker-compose.production.yml restart + +# Specific service +docker compose -f docker-compose.production.yml restart django +docker compose -f docker-compose.production.yml restart activepieces +``` + +### Django Shell + +```bash +docker compose -f docker-compose.production.yml exec django python manage.py shell +``` + +### Database Access + +```bash +# SmoothSchedule database +docker compose -f docker-compose.production.yml exec postgres \ + psql -U -d smoothschedule + +# Activepieces database +docker compose -f docker-compose.production.yml exec postgres \ + psql -U -d activepieces +``` + +### Common Issues + +**1. Activepieces pieces not showing up** +```bash +# Check if platform exists +docker compose -f docker-compose.production.yml exec postgres \ + psql -U -d activepieces -c "SELECT id FROM platform" + +# Restart to re-run piece registration +docker compose -f docker-compose.production.yml restart activepieces + +# Check logs for errors +docker compose -f docker-compose.production.yml logs activepieces | grep -i error +``` + +**2. 502 Bad Gateway** +- Service is still starting, wait a moment +- Check container health: `docker compose ps` +- Check logs for errors + +**3. Database connection errors** +- Verify credentials in `.envs/.production/` +- Ensure PostgreSQL is running: `docker compose ps postgres` + +**4. Activepieces embedding not working** +- Verify `AP_JWT_SECRET` matches in both `.django` and `.activepieces` +- Verify `AP_PLATFORM_ID` is set correctly in both files +- Check `AP_EMBEDDING_ENABLED=true` in `.activepieces` + +**5. SSL certificate issues** +```bash +# Check Traefik logs +docker compose -f docker-compose.production.yml logs traefik + +# Verify DNS is pointing to server +dig yourdomain.com +short + +# Ensure ports 80 and 443 are open +sudo ufw allow 80 +sudo ufw allow 443 +``` + +## Maintenance + +### Backups ```bash # Database backup @@ -329,121 +388,50 @@ docker compose -f docker-compose.production.yml exec postgres backups docker compose -f docker-compose.production.yml exec postgres restore backup_filename.sql.gz ``` -### 3. Updates +### Monitoring -```bash -# Pull latest code -cd ~/smoothschedule/smoothschedule -git pull origin main +- **Flower Dashboard**: `https://yourdomain.com:5555` - Celery task monitoring +- **Container Status**: `docker compose ps` +- **Resource Usage**: `docker stats` -# Rebuild and restart -docker compose -f docker-compose.production.yml build -docker compose -f docker-compose.production.yml up -d +### Security Checklist -# Run migrations -docker compose -f docker-compose.production.yml exec django python manage.py migrate - -# Collect static files -docker compose -f docker-compose.production.yml exec django python manage.py collectstatic --noinput -``` - -## Troubleshooting - -### SSL Certificate Issues -```bash -# Check Traefik logs -docker compose -f docker-compose.production.yml logs traefik - -# Verify DNS is pointing to server -dig smoothschedule.com +short - -# Ensure ports are open -sudo ufw allow 80 -sudo ufw allow 443 -``` - -### Database Connection Issues -```bash -# Check PostgreSQL is running -docker compose -f docker-compose.production.yml ps postgres - -# Check database logs -docker compose -f docker-compose.production.yml logs postgres - -# Verify connection -docker compose -f docker-compose.production.yml exec django python manage.py dbshell -``` - -### Static Files Not Loading -```bash -# Verify DigitalOcean Spaces credentials -docker compose -f docker-compose.production.yml exec django python manage.py shell ->>> from django.conf import settings ->>> print(settings.AWS_ACCESS_KEY_ID) ->>> print(settings.AWS_STORAGE_BUCKET_NAME) - -# Re-collect static files -docker compose -f docker-compose.production.yml exec django python manage.py collectstatic --noinput - -# Check Spaces bucket -aws --profile do-tor1 s3 ls s3://smoothschedule/static/ -aws --profile do-tor1 s3 ls s3://smoothschedule/media/ -``` - -### Celery Not Running Tasks -```bash -# Check Celery worker logs -docker compose -f docker-compose.production.yml logs celeryworker - -# Access Flower dashboard -# https://smoothschedule.com:5555 - -# Restart Celery -docker compose -f docker-compose.production.yml restart celeryworker celerybeat -``` - -## Security Checklist - -- [x] SSL/HTTPS enabled via Let's Encrypt -- [x] DJANGO_SECRET_KEY set to random value -- [x] Database password set to random value -- [x] Flower dashboard password protected -- [ ] Firewall configured (UFW or iptables) -- [ ] SSH key-based authentication enabled -- [ ] Fail2ban installed for brute-force protection +- [x] SSL/HTTPS enabled via Let's Encrypt (automatic with Traefik) +- [x] All secret keys are unique random values +- [x] Database passwords are strong +- [x] Flower dashboard is password protected +- [ ] Firewall configured (UFW) +- [ ] SSH key-based authentication only - [ ] Regular backups configured -- [ ] Sentry error monitoring (optional) +- [ ] Monitoring/alerting set up -## Performance Optimization +## File Structure -1. **Enable CDN for DigitalOcean Spaces** - - In Spaces settings, enable CDN - - Update `DJANGO_AWS_S3_CUSTOM_DOMAIN=smoothschedule.nyc3.cdn.digitaloceanspaces.com` - -2. **Scale Gunicorn Workers** - - Adjust `WEB_CONCURRENCY` in `.envs/.production/.django` - - Formula: (2 x CPU cores) + 1 - -3. **Add Redis Persistence** - - Update docker-compose.production.yml redis config - - Enable AOF persistence - -4. **Database Connection Pooling** - - Already configured via `CONN_MAX_AGE=60` - -## Maintenance - -### Weekly -- Review error logs -- Check disk space: `df -h` -- Monitor Flower dashboard for failed tasks - -### Monthly -- Update Docker images: `docker compose pull` -- Update dependencies: `uv sync` -- Review backups - -### As Needed -- Scale resources (CPU/RAM) -- Add more Celery workers -- Optimize database queries +``` +smoothschedule/ +├── deploy.sh # Main deployment script +├── DEPLOYMENT.md # This file +├── scripts/ +│ └── build-activepieces.sh # Activepieces image builder +├── smoothschedule/ +│ ├── docker-compose.production.yml +│ ├── scripts/ +│ │ └── init-production.sh # One-time initialization +│ ├── .envs/ +│ │ └── .production/ # Production secrets (NOT in git) +│ │ ├── .django +│ │ ├── .postgres +│ │ └── .activepieces +│ └── .envs.example/ # Template files (in git) +│ ├── .django +│ ├── .postgres +│ └── .activepieces +└── activepieces-fork/ + ├── Dockerfile + ├── custom-pieces-metadata.sql + ├── publish-pieces.sh + └── packages/pieces/community/ + ├── smoothschedule/ # Main custom piece + ├── python-code/ + └── ruby-code/ +``` diff --git a/activepieces-fork/.nx/cache/cloud/verify.lock b/activepieces-fork/.nx/cache/cloud/verify.lock index 0f9c3211..fd3a525a 100644 --- a/activepieces-fork/.nx/cache/cloud/verify.lock +++ b/activepieces-fork/.nx/cache/cloud/verify.lock @@ -1 +1 @@ -1766209168989 \ No newline at end of file +1766280110308 \ No newline at end of file diff --git a/activepieces-fork/Dockerfile b/activepieces-fork/Dockerfile index ca239d6a..e4b0080b 100644 --- a/activepieces-fork/Dockerfile +++ b/activepieces-fork/Dockerfile @@ -1,11 +1,15 @@ FROM node:20.19-bullseye-slim AS base # Set environment variables early for better layer caching +# Memory optimizations for low-RAM servers (2GB): +# - Limit Node.js heap to 1536MB to leave room for system +# - Disable NX daemon and cloud to reduce overhead ENV LANG=en_US.UTF-8 \ LANGUAGE=en_US:en \ LC_ALL=en_US.UTF-8 \ NX_DAEMON=false \ - NX_NO_CLOUD=true + NX_NO_CLOUD=true \ + NODE_OPTIONS="--max-old-space-size=1536" # Install all system dependencies in a single layer with cache mounts RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ @@ -63,7 +67,7 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \ COPY . . # Build all projects including custom pieces -RUN npx nx run-many --target=build --projects=react-ui,server-api,pieces-smoothschedule,pieces-python-code,pieces-ruby-code --configuration production --parallel=2 --skip-nx-cache --verbose +RUN npx nx run-many --target=build --projects=react-ui,server-api,pieces-smoothschedule,pieces-python-code,pieces-ruby-code,pieces-interfaces --configuration production --parallel=2 --skip-nx-cache # Install production dependencies only for the backend API RUN --mount=type=cache,target=/root/.bun/install/cache \ @@ -77,6 +81,8 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \ cd ../python-code && \ bun install --production && \ cd ../ruby-code && \ + bun install --production && \ + cd ../interfaces && \ bun install --production ### STAGE 2: Run ### @@ -84,24 +90,30 @@ FROM base AS run WORKDIR /usr/src/app -# Install Nginx and gettext in a single layer with cache mount +# Install Nginx, gettext, and PostgreSQL client in a single layer with cache mount RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update && \ - apt-get install -y --no-install-recommends nginx gettext + apt-get install -y --no-install-recommends nginx gettext postgresql-client # Copy static configuration files first (better layer caching) COPY nginx.react.conf /etc/nginx/nginx.conf COPY --from=build /usr/src/app/packages/server/api/src/assets/default.cf /usr/local/etc/isolate COPY docker-entrypoint.sh . +COPY custom-pieces-metadata.sql . +COPY publish-pieces.sh . # Create all necessary directories in one layer +# Also create symlink for AP_DEV_PIECES to find pieces in dist folder +# Structure: /packages/pieces/community -> /dist/packages/pieces/community RUN mkdir -p \ /usr/src/app/dist/packages/server \ /usr/src/app/dist/packages/engine \ /usr/src/app/dist/packages/shared \ - /usr/src/app/dist/packages/pieces && \ - chmod +x docker-entrypoint.sh + /usr/src/app/dist/packages/pieces \ + /usr/src/app/packages/pieces && \ + ln -sf /usr/src/app/dist/packages/pieces/community /usr/src/app/packages/pieces/community && \ + chmod +x docker-entrypoint.sh publish-pieces.sh # Copy built artifacts from build stage COPY --from=build /usr/src/app/LICENSE . diff --git a/activepieces-fork/custom-pieces-metadata.sql b/activepieces-fork/custom-pieces-metadata.sql new file mode 100644 index 00000000..5fcbddf1 --- /dev/null +++ b/activepieces-fork/custom-pieces-metadata.sql @@ -0,0 +1,123 @@ +-- ============================================================================== +-- Custom SmoothSchedule Pieces Metadata +-- ============================================================================== +-- This script registers custom pieces in the Activepieces database. +-- It runs on container startup via publish-pieces.sh. +-- +-- IMPORTANT: +-- - Pieces use pieceType=CUSTOM with platformId to avoid being deleted by sync +-- - This script is IDEMPOTENT - safe to run multiple times +-- - If platform doesn't exist yet, this script will silently skip +-- ============================================================================== + +-- Get the platform ID dynamically and only proceed if platform exists +DO $$ +DECLARE + platform_id varchar(21); + platform_count integer; +BEGIN + -- Check if platform table exists and has data + SELECT COUNT(*) INTO platform_count FROM platform; + + IF platform_count = 0 THEN + RAISE NOTICE 'No platform found yet - skipping piece metadata registration'; + RAISE NOTICE 'Pieces will be registered on next container restart after platform is created'; + RETURN; + END IF; + + SELECT id INTO platform_id FROM platform LIMIT 1; + RAISE NOTICE 'Registering custom pieces for platform: %', platform_id; + + -- Pin our custom pieces in the platform so they appear first + UPDATE platform + SET "pinnedPieces" = ARRAY[ + '@activepieces/piece-smoothschedule', + '@activepieces/piece-python-code', + '@activepieces/piece-ruby-code' + ]::varchar[] + WHERE id = platform_id + AND ("pinnedPieces" = '{}' OR "pinnedPieces" IS NULL OR NOT '@activepieces/piece-smoothschedule' = ANY("pinnedPieces")); + + -- Delete existing entries for our custom pieces (to avoid ID conflicts) + DELETE FROM piece_metadata WHERE name IN ( + '@activepieces/piece-smoothschedule', + '@activepieces/piece-python-code', + '@activepieces/piece-ruby-code', + '@activepieces/piece-interfaces' + ); + + -- SmoothSchedule piece + INSERT INTO piece_metadata ( + id, name, "displayName", "logoUrl", description, version, + "minimumSupportedRelease", "maximumSupportedRelease", + actions, triggers, auth, "pieceType", "packageType", categories, authors, "projectUsage", "platformId" + ) VALUES ( + 'smoothschedule001', + '@activepieces/piece-smoothschedule', + 'SmoothSchedule', + 'https://api.smoothschedule.com/images/logo-branding.png', + 'Scheduling and appointment management for your business', + '0.0.1', + '0.36.1', + '99999.99999.9999', + '{"create_event":{"name":"create_event","displayName":"Create Event","description":"Create a new event/appointment","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"update_event":{"name":"update_event","displayName":"Update Event","description":"Update an existing event","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"cancel_event":{"name":"cancel_event","displayName":"Cancel Event","description":"Cancel an event","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"find_events":{"name":"find_events","displayName":"Find Events","description":"Search for events","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"list_resources":{"name":"list_resources","displayName":"List Resources","description":"List all resources","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"list_services":{"name":"list_services","displayName":"List Services","description":"List all services","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"list_inactive_customers":{"name":"list_inactive_customers","displayName":"List Inactive Customers","description":"List customers who havent booked recently","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"send_email":{"name":"send_email","displayName":"Send Email","description":"Send an email using a SmoothSchedule email template","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"list_email_templates":{"name":"list_email_templates","displayName":"List Email Templates","description":"Get all available email templates","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}},"custom_api_call":{"name":"custom_api_call","displayName":"Custom API Call","description":"Make a custom API request","props":{},"requireAuth":true,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}}}', + '{"event_created":{"name":"event_created","displayName":"Event Created","description":"Triggers when a new event is created","props":{},"type":"WEBHOOK","handshakeConfiguration":{"strategy":"NONE"},"requireAuth":true,"testStrategy":"SIMULATION"},"event_updated":{"name":"event_updated","displayName":"Event Updated","description":"Triggers when an event is updated","props":{},"type":"WEBHOOK","handshakeConfiguration":{"strategy":"NONE"},"requireAuth":true,"testStrategy":"SIMULATION"},"event_cancelled":{"name":"event_cancelled","displayName":"Event Cancelled","description":"Triggers when an event is cancelled","props":{},"type":"WEBHOOK","handshakeConfiguration":{"strategy":"NONE"},"requireAuth":true,"testStrategy":"SIMULATION"},"event_status_changed":{"name":"event_status_changed","displayName":"Event Status Changed","description":"Triggers when event status changes","props":{},"type":"WEBHOOK","handshakeConfiguration":{"strategy":"NONE"},"requireAuth":true,"testStrategy":"SIMULATION"}}', + '{"type":"CUSTOM_AUTH","displayName":"Connection","description":"Connect to your SmoothSchedule account","required":true,"props":{"baseUrl":{"displayName":"API URL","description":"Your SmoothSchedule API URL","required":true,"type":"SECRET_TEXT"},"apiToken":{"displayName":"API Token","description":"Your API token from Settings","required":true,"type":"SECRET_TEXT"}}}', + 'CUSTOM', + 'REGISTRY', + ARRAY['PRODUCTIVITY', 'SALES_AND_CRM'], + ARRAY['smoothschedule'], + 100, + platform_id + ); + + -- Python Code piece + INSERT INTO piece_metadata ( + id, name, "displayName", "logoUrl", description, version, + "minimumSupportedRelease", "maximumSupportedRelease", + actions, triggers, auth, "pieceType", "packageType", categories, authors, "projectUsage", "platformId" + ) VALUES ( + 'pythoncode00001', + '@activepieces/piece-python-code', + 'Python Code', + 'https://api.smoothschedule.com/images/python-logo.svg', + 'Execute custom Python code in your workflows', + '0.0.1', + '0.36.1', + '99999.99999.9999', + '{"run_python":{"name":"run_python","displayName":"Run Python Code","description":"Execute Python code and return results","props":{"code":{"displayName":"Python Code","description":"The Python code to execute","required":true,"type":"LONG_TEXT"}},"requireAuth":false,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}}}', + '{}', + NULL, + 'CUSTOM', + 'REGISTRY', + ARRAY['DEVELOPER_TOOLS'], + ARRAY['smoothschedule'], + 0, + platform_id + ); + + -- Ruby Code piece + INSERT INTO piece_metadata ( + id, name, "displayName", "logoUrl", description, version, + "minimumSupportedRelease", "maximumSupportedRelease", + actions, triggers, auth, "pieceType", "packageType", categories, authors, "projectUsage", "platformId" + ) VALUES ( + 'rubycode000001', + '@activepieces/piece-ruby-code', + 'Ruby Code', + 'https://api.smoothschedule.com/images/ruby-logo.svg', + 'Execute custom Ruby code in your workflows', + '0.0.1', + '0.36.1', + '99999.99999.9999', + '{"run_ruby":{"name":"run_ruby","displayName":"Run Ruby Code","description":"Execute Ruby code and return results","props":{"code":{"displayName":"Ruby Code","description":"The Ruby code to execute","required":true,"type":"LONG_TEXT"}},"requireAuth":false,"errorHandlingOptions":{"continueOnFailure":{"defaultValue":false},"retryOnFailure":{"defaultValue":false}}}}', + '{}', + NULL, + 'CUSTOM', + 'REGISTRY', + ARRAY['DEVELOPER_TOOLS'], + ARRAY['smoothschedule'], + 0, + platform_id + ); +END $$; diff --git a/activepieces-fork/docker-entrypoint.sh b/activepieces-fork/docker-entrypoint.sh index 5d3855d9..c603ce82 100644 --- a/activepieces-fork/docker-entrypoint.sh +++ b/activepieces-fork/docker-entrypoint.sh @@ -12,6 +12,10 @@ echo "AP_FAVICON_URL: $AP_FAVICON_URL" envsubst '${AP_APP_TITLE} ${AP_FAVICON_URL}' < /usr/share/nginx/html/index.html > /usr/share/nginx/html/index.html.tmp && \ mv /usr/share/nginx/html/index.html.tmp /usr/share/nginx/html/index.html +# Register custom pieces (publish to Verdaccio and insert metadata) +if [ -f /usr/src/app/publish-pieces.sh ]; then + /usr/src/app/publish-pieces.sh || echo "Warning: Custom pieces registration had issues" +fi # Start Nginx server nginx -g "daemon off;" & diff --git a/activepieces-fork/nginx.react.conf b/activepieces-fork/nginx.react.conf index d4a3fa7a..84c42d95 100644 --- a/activepieces-fork/nginx.react.conf +++ b/activepieces-fork/nginx.react.conf @@ -31,7 +31,7 @@ http { proxy_send_timeout 900s; } - location ~* ^/(?!api/).*.(css|js|jpg|jpeg|png|gif|ico|svg)$ { + location ~* ^/(?!api/).*\.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ { root /usr/share/nginx/html; add_header Expires "0"; add_header Cache-Control "public, max-age=31536000, immutable"; diff --git a/activepieces-fork/packages/pieces/community/interfaces/package.json b/activepieces-fork/packages/pieces/community/interfaces/package.json new file mode 100644 index 00000000..f793b570 --- /dev/null +++ b/activepieces-fork/packages/pieces/community/interfaces/package.json @@ -0,0 +1,4 @@ +{ + "name": "@activepieces/piece-interfaces", + "version": "0.0.1" +} diff --git a/activepieces-fork/packages/pieces/community/interfaces/project.json b/activepieces-fork/packages/pieces/community/interfaces/project.json new file mode 100644 index 00000000..9e0f0a2a --- /dev/null +++ b/activepieces-fork/packages/pieces/community/interfaces/project.json @@ -0,0 +1,50 @@ +{ + "name": "pieces-interfaces", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/pieces/community/interfaces/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "dist/packages/pieces/community/interfaces", + "tsConfig": "packages/pieces/community/interfaces/tsconfig.lib.json", + "packageJson": "packages/pieces/community/interfaces/package.json", + "main": "packages/pieces/community/interfaces/src/index.ts", + "assets": [], + "buildableProjectDepsInPackageJsonType": "dependencies", + "updateBuildableProjectDepsInPackageJson": true + }, + "dependsOn": [ + "^build", + "prebuild" + ] + }, + "publish": { + "command": "node tools/scripts/publish.mjs pieces-interfaces {args.ver} {args.tag}", + "dependsOn": [ + "build" + ] + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": [ + "{options.outputFile}" + ] + }, + "prebuild": { + "executor": "nx:run-commands", + "options": { + "cwd": "packages/pieces/community/interfaces", + "command": "bun install --no-save --silent" + }, + "dependsOn": [ + "^build" + ] + } + }, + "tags": [] +} diff --git a/activepieces-fork/packages/pieces/community/interfaces/src/index.ts b/activepieces-fork/packages/pieces/community/interfaces/src/index.ts new file mode 100644 index 00000000..4c279c3a --- /dev/null +++ b/activepieces-fork/packages/pieces/community/interfaces/src/index.ts @@ -0,0 +1,14 @@ +import { createPiece, PieceAuth } from '@activepieces/pieces-framework'; +import { PieceCategory } from '@activepieces/shared'; + +export const interfaces = createPiece({ + displayName: 'Interfaces', + description: 'Create custom forms and interfaces for your workflows.', + auth: PieceAuth.None(), + categories: [PieceCategory.CORE], + minimumSupportedRelease: '0.52.0', + logoUrl: 'https://cdn.activepieces.com/pieces/interfaces.svg', + authors: ['activepieces'], + actions: [], + triggers: [], +}); diff --git a/activepieces-fork/packages/pieces/community/interfaces/tsconfig.json b/activepieces-fork/packages/pieces/community/interfaces/tsconfig.json new file mode 100644 index 00000000..29c9dd1b --- /dev/null +++ b/activepieces-fork/packages/pieces/community/interfaces/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/activepieces-fork/packages/pieces/community/interfaces/tsconfig.lib.json b/activepieces-fork/packages/pieces/community/interfaces/tsconfig.lib.json new file mode 100644 index 00000000..28369ef7 --- /dev/null +++ b/activepieces-fork/packages/pieces/community/interfaces/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/activepieces-fork/packages/pieces/community/smoothschedule/src/index.ts b/activepieces-fork/packages/pieces/community/smoothschedule/src/index.ts index 1379b40a..cd7c8858 100644 --- a/activepieces-fork/packages/pieces/community/smoothschedule/src/index.ts +++ b/activepieces-fork/packages/pieces/community/smoothschedule/src/index.ts @@ -5,6 +5,8 @@ import { createEventAction, findEventsAction, updateEventAction, cancelEventActi import { listResourcesAction } from './lib/actions/list-resources'; import { listServicesAction } from './lib/actions/list-services'; import { listInactiveCustomersAction } from './lib/actions/list-inactive-customers'; +import { sendEmailAction } from './lib/actions/send-email'; +import { listEmailTemplatesAction } from './lib/actions/list-email-templates'; import { eventCreatedTrigger, eventUpdatedTrigger, eventCancelledTrigger, eventStatusChangedTrigger } from './lib/triggers'; import { API_URL } from './lib/common'; @@ -75,6 +77,8 @@ export const smoothSchedule = createPiece({ listResourcesAction, listServicesAction, listInactiveCustomersAction, + sendEmailAction, + listEmailTemplatesAction, createCustomApiCallAction({ auth: smoothScheduleAuth, baseUrl: (auth) => (auth as SmoothScheduleAuth)?.props?.baseUrl ?? '', diff --git a/activepieces-fork/packages/pieces/community/smoothschedule/src/lib/actions/list-email-templates.ts b/activepieces-fork/packages/pieces/community/smoothschedule/src/lib/actions/list-email-templates.ts new file mode 100644 index 00000000..339f254b --- /dev/null +++ b/activepieces-fork/packages/pieces/community/smoothschedule/src/lib/actions/list-email-templates.ts @@ -0,0 +1,23 @@ +import { createAction } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index'; +import { makeRequest } from '../common'; + +export const listEmailTemplatesAction = createAction({ + auth: smoothScheduleAuth, + name: 'list_email_templates', + displayName: 'List Email Templates', + description: 'Get all available email templates (system and custom)', + props: {}, + async run(context) { + const auth = context.auth as SmoothScheduleAuth; + + const response = await makeRequest( + auth, + HttpMethod.GET, + '/emails/templates/' + ); + + return response; + }, +}); diff --git a/activepieces-fork/packages/pieces/community/smoothschedule/src/lib/actions/send-email.ts b/activepieces-fork/packages/pieces/community/smoothschedule/src/lib/actions/send-email.ts new file mode 100644 index 00000000..a81db365 --- /dev/null +++ b/activepieces-fork/packages/pieces/community/smoothschedule/src/lib/actions/send-email.ts @@ -0,0 +1,112 @@ +import { Property, createAction } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index'; +import { makeRequest } from '../common'; + +export const sendEmailAction = createAction({ + auth: smoothScheduleAuth, + name: 'send_email', + displayName: 'Send Email', + description: 'Send an email using a SmoothSchedule email template', + props: { + template_type: Property.StaticDropdown({ + displayName: 'Template Type', + description: 'Choose whether to use a system template or a custom template', + required: true, + options: { + options: [ + { label: 'System Template', value: 'system' }, + { label: 'Custom Template', value: 'custom' }, + ], + }, + }), + email_type: Property.StaticDropdown({ + displayName: 'System Email Type', + description: 'Select a system email template', + required: false, + options: { + options: [ + { label: 'Appointment Confirmation', value: 'appointment_confirmation' }, + { label: 'Appointment Reminder', value: 'appointment_reminder' }, + { label: 'Appointment Rescheduled', value: 'appointment_rescheduled' }, + { label: 'Appointment Cancelled', value: 'appointment_cancelled' }, + { label: 'Welcome Email', value: 'welcome_email' }, + { label: 'Password Reset', value: 'password_reset' }, + { label: 'Invoice', value: 'invoice' }, + { label: 'Payment Receipt', value: 'payment_receipt' }, + { label: 'Staff Invitation', value: 'staff_invitation' }, + { label: 'Customer Winback', value: 'customer_winback' }, + ], + }, + }), + template_slug: Property.ShortText({ + displayName: 'Custom Template Slug', + description: 'The slug/identifier of your custom email template', + required: false, + }), + to_email: Property.ShortText({ + displayName: 'Recipient Email', + description: 'The email address to send to', + required: true, + }), + subject_override: Property.ShortText({ + displayName: 'Subject Override', + description: 'Override the template subject (optional)', + required: false, + }), + reply_to: Property.ShortText({ + displayName: 'Reply-To Email', + description: 'Reply-to email address (optional)', + required: false, + }), + context: Property.Object({ + displayName: 'Template Variables', + description: 'Variables to replace in the template (e.g., customer_name, appointment_date)', + required: false, + }), + }, + async run(context) { + const { template_type, email_type, template_slug, to_email, subject_override, reply_to, context: templateContext } = context.propsValue; + const auth = context.auth as SmoothScheduleAuth; + + // Validate that the right template identifier is provided based on type + if (template_type === 'system' && !email_type) { + throw new Error('System Email Type is required when using System Template'); + } + if (template_type === 'custom' && !template_slug) { + throw new Error('Custom Template Slug is required when using Custom Template'); + } + + // Build the request body + const requestBody: Record = { + to_email, + }; + + if (template_type === 'system') { + requestBody['email_type'] = email_type; + } else { + requestBody['template_slug'] = template_slug; + } + + if (subject_override) { + requestBody['subject_override'] = subject_override; + } + + if (reply_to) { + requestBody['reply_to'] = reply_to; + } + + if (templateContext && Object.keys(templateContext).length > 0) { + requestBody['context'] = templateContext; + } + + const response = await makeRequest( + auth, + HttpMethod.POST, + '/emails/send/', + requestBody + ); + + return response; + }, +}); diff --git a/activepieces-fork/packages/react-ui/src/app/builder/step-settings/piece-settings/connection-select.tsx b/activepieces-fork/packages/react-ui/src/app/builder/step-settings/piece-settings/connection-select.tsx index d5d812a8..8d18c6eb 100644 --- a/activepieces-fork/packages/react-ui/src/app/builder/step-settings/piece-settings/connection-select.tsx +++ b/activepieces-fork/packages/react-ui/src/app/builder/step-settings/piece-settings/connection-select.tsx @@ -1,6 +1,6 @@ import { t } from 'i18next'; import { Plus, Globe } from 'lucide-react'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useFormContext } from 'react-hook-form'; import { AutoFormFieldWrapper } from '@/app/builder/piece-properties/auto-form-field-wrapper'; @@ -80,6 +80,27 @@ function ConnectionSelect(params: ConnectionSelectProps) { PropertyExecutionType.DYNAMIC; const isPLatformAdmin = useIsPlatformAdmin(); + // Auto-select connection with autoSelect metadata if no connection is selected + useEffect(() => { + if (isLoadingConnections || !connections?.data) return; + + const currentAuth = form.getValues().settings.input.auth; + // Only auto-select if no connection is currently selected + if (currentAuth && removeBrackets(currentAuth)) return; + + // Find a connection with autoSelect metadata + const autoSelectConnection = connections.data.find( + (connection) => (connection as any).metadata?.autoSelect === true + ); + + if (autoSelectConnection) { + form.setValue('settings.input.auth', addBrackets(autoSelectConnection.externalId), { + shouldValidate: true, + shouldDirty: true, + }); + } + }, [connections?.data, isLoadingConnections, form]); + return ( { - const { filteredTemplates, isLoading, search, setSearch } = useTemplates({ - type: TemplateType.CUSTOM, - }); + const { filteredTemplates, isLoading, search, setSearch } = useAllTemplates(); const carousel = useRef(); const [selectedTemplate, setSelectedTemplate] = useState