diff --git a/Contributing.md b/Contributing.md new file mode 100644 index 0000000..00dd5a6 --- /dev/null +++ b/Contributing.md @@ -0,0 +1,316 @@ +# Contributing to MeshHook + +Thank you for your interest in contributing to MeshHook! This guide will help you get started with contributing to our webhook-first workflow engine. + +## About MeshHook + +MeshHook is an MIT-licensed, webhook-first workflow engine with a visual builder and Temporal-like durability via event sourcing on Postgres. We mesh your webhooks and orchestrate everything with simplicity, durability, and security. + +--- + +## πŸš€ Getting Started + +### Prerequisites + +Before you begin, ensure you have: + +- **Node.js** (v18 or later) or **Bun** +- **pnpm** (recommended package manager) +- **Git** +- **Supabase CLI** (`pnpm install -g supabase`) +- **Docker** (for local Supabase) + +### Setting Up Your Development Environment + +**1. Fork and clone** +```bash +git clone https://github.com/YOUR_USERNAME/meshhook.git +cd meshhook +``` + +**2. Install dependencies** +```bash +pnpm install +``` + +**3. Setup environment** +```bash +pnpm run setup +``` +Select "Local Development" when prompted. + +**4. Start Supabase locally** +```bash +pnpx supabase start +``` + +**5. Run database migrations** +```bash +pnpm run db:migrate +``` + +**6. Start the orchestrator** +```bash +pnpm run start +``` + +Your local instance should now be running at `http://localhost:8080` + +### Tech Stack + +- **UI/API**: SvelteKit (Svelte 5) +- **Database/Queues/Realtime/Storage**: Supabase (Postgres) +- **Workers**: Node.js or Bun (stateless) +- **Queue**: pg-boss or pgmq (Postgres-native) +- **Transforms**: JMESPath +- **Single Service**: All components run on port 8080 + +--- + +## How to Contribute + +### Contribution Workflow + +**1. Create a feature branch** +```bash +git checkout -b feature/your-feature-name +# or for bug fixes +git checkout -b fix/bug-description +``` + +**2. Make your changes** +- Write clean, readable code +- Follow our style guide +- Add tests if applicable +- Update documentation if needed + +**3. Test your changes** +```bash +pnpm run lint +pnpm run start +pnpm run db:migrate # if you changed the database +``` + +**4. Commit your changes** +```bash +git add . +git commit -m "Brief description of your changes" +``` + +Good commit messages: +- βœ… `Add webhook retry logic` +- βœ… `Fix event sourcing race condition` +- βœ… `Update JMESPath transform examples` + +**5. Push to your fork** +```bash +git push origin feature/your-feature-name +``` + +**6. Create a Pull Request** + +Go to your fork on GitHub and click "New Pull Request". Fill out the PR template with: +- Description of changes +- Related issue number (if applicable) +- Testing performed +- Screenshots (for UI changes) + +**7. Address review feedback** +- Respond to comments promptly +- Make requested changes +- Push updates to the same branch + +### What to Contribute + +We welcome contributions in many forms: + +- **Bug fixes** - Found a bug? Please fix it! +- **New features** - Have an idea? Open an issue first to discuss +- **Documentation** - Improve guides, add examples, fix typos +- **Tests** - Add missing test coverage +- **Performance improvements** - Make MeshHook faster +- **Webhook integrations** - Add new webhook sources or destinations +- **JMESPath transforms** - Create reusable transform templates +- **UI/UX enhancements** - Improve the visual builder + +Looking for your first contribution? Check issues labeled `good first issue` or `hacktoberfest`. + +--- + +## πŸ“‹ Style Guide + +### JavaScript/TypeScript + +- Use **TypeScript** for new code +- Use `const` and `let`, avoid `var` +- Prefer arrow functions for callbacks +- Use async/await over raw promises +- Name variables descriptively (avoid single letters except in loops) +- Add JSDoc comments for functions and complex logic + +### Svelte 5 + +- Use **Svelte 5 runes** (`$state`, `$derived`, `$effect`) +- Keep components small and focused +- Use TypeScript in script blocks +- Follow reactive programming patterns +- Extract reusable logic into composables + +### CSS + +- Use **Tailwind CSS** utility classes when possible +- For custom styles, use component-scoped CSS +- Follow mobile-first responsive design +- Maintain consistent spacing using Tailwind's scale + +### Database & Event Sourcing + +- Use **Postgres-native** features whenever possible +- Follow event sourcing patterns for workflow state +- Use pg-boss or pgmq for queue operations +- Leverage Supabase Realtime for live updates +- Write efficient queries, considering partitioning strategy + +### JMESPath Transforms + +- Keep transforms simple and readable +- Add comments explaining complex expressions +- Test transforms with sample data +- Document expected input/output structures + +--- + +## Community Standards + +### Code of Conduct + +We are committed to providing a welcoming and inclusive environment. We expect all contributors to: + +- **Be respectful** - Treat everyone with kindness and empathy +- **Be collaborative** - Work together, share knowledge, help others +- **Be patient** - Remember that everyone is learning +- **Be constructive** - Provide helpful feedback, focus on solutions +- **Be inclusive** - Welcome people of all backgrounds and skill levels + +### Communication + +- **GitHub Issues** - For bug reports and feature requests +- **Pull Request comments** - For code-specific discussions +- **Project discussions** - For general questions and ideas + +### Reporting Issues + +When reporting a bug, please include: +- Clear description of the issue +- Steps to reproduce +- Expected vs actual behavior +- Environment details (OS, Node version, Supabase version) +- Screenshots or error logs if applicable +- Relevant workflow configuration (if applicable) + +--- + +## πŸ”’ Security + +- **Never commit sensitive information** (API keys, passwords, tokens, Supabase credentials) +- Use environment variables for all configuration +- `.env.staging` and `.env.production` are **not committed** to the repository +- Report security vulnerabilities privately to the maintainers +- Follow security best practices in webhook handling and data transformations + +--- + +## βœ… Testing + +Before submitting a PR: +- Test locally with `pnpm run start` +- Verify migrations work: `pnpm run db:migrate` +- Test webhook intake and workflow execution +- Check for linting errors: `pnpm run lint` +- Verify that existing functionality still works +- Test with both local Supabase and remote instances (if possible) + +--- + +## πŸ“š Documentation + +When adding new features: +- Update relevant documentation in `./docs/` +- Add code comments for complex logic +- Update PlantUML diagrams if architecture changes +- Add examples for new webhook integrations or transforms +- Update the README if necessary + +--- + +## Useful Commands + +### Setup & Configuration +```bash +pnpm run setup # Interactive environment configuration +pnpm run db:migrate # Run database migrations (auto-detects environment) +``` + +### Development +```bash +pnpm run start # Start the orchestrator worker +pnpm mh --help # CLI help +``` + +### Testing & Linting +```bash +pnpm test # Run tests (if available) +pnpm run lint # Check for linting errors +``` + +### Supabase +```bash +pnpx supabase start # Start local Supabase +pnpx supabase stop # Stop local Supabase +pnpx supabase status # Check Supabase status +``` + +--- + +## Environment Files + +- `.env.local` - Local development (committed to repo with safe defaults) +- `.env.staging` - Staging environment (**not committed**) +- `.env.production` - Production environment (**not committed**) +- `.env` - Symlink to active environment (created by setup script) + +**Never commit `.env.staging` or `.env.production`!** + +--- + +## Architecture References + +Before making architectural changes, review: +- `./docs/Architecture.md` - System architecture overview +- `./docs/Event-Partitioning.md` - Event sourcing and partitioning strategy +- `./docs/PRD.md` - Product requirements and design decisions +- `./docs/diagrams/*.puml` - Visual architecture diagrams + +--- + +## Questions? + +If you're unsure about anything: +- Check existing documentation in `./docs/` +- Review similar implementations in the codebase +- Open a new issue with the "question" label +- Reach out to maintainers through project communication channels + +--- + +## Recognition + +All contributors are valued and will be recognized in our project documentation. Thank you for helping make MeshHook better! + +--- + +**Happy Contributing! πŸŽ‰** + +*MeshHook β€” Mesh your webhooks. Orchestrate everything.* + +This guide is maintained by the MeshHook community. If you find areas for improvement, please submit a PR! \ No newline at end of file diff --git a/Darkmode.md b/Darkmode.md new file mode 100644 index 0000000..0a8e1a4 --- /dev/null +++ b/Darkmode.md @@ -0,0 +1,16 @@ +# Dark Mode Implementation + +## Overview +MeshHook supports dark mode with automatic persistence across sessions and devices. + +## Usage +- Click the moon/sun icon in the header to toggle +- Theme preference is saved locally and synced to your account + +## For Developers +- Theme colors are defined in `static/styles/themes.css` +- Theme state is managed in `src/lib/stores/theme.js` +- Use CSS variables for all colors in components + +## CSS Variables +See `static/styles/themes.css` for the complete list of theme variables. \ No newline at end of file diff --git a/DeploymentGuide.md b/DeploymentGuide.md new file mode 100644 index 0000000..3bf90ed --- /dev/null +++ b/DeploymentGuide.md @@ -0,0 +1,831 @@ +# πŸš€ MeshHook Deployment Guide + +> Webhook-first, deterministic, Postgres-native workflow engine + +## πŸ“‘ Quick Links + +- [πŸ“‹ Prerequisites](#-prerequisites) +- [⚑ Setup](#-setup) +- [πŸ”§ Configuration](#-configuration) +- [🌍 Deploy](#-deploy) +- [πŸ” Security](#-security) +- [πŸ“Š Monitor](#-monitor) +- [πŸ“ˆ Scale](#-scale) +- [πŸ”„ Updates](#-updates) +- [πŸ› Troubleshoot](#-troubleshoot) + +--- + +## πŸ“‹ Prerequisites + +| Component | Version | +|-----------|---------| +| 🟒 Node.js | 18.x+ | +| πŸ’Ύ RAM | 2 GB+ | +| πŸ’Ώ Storage | 10 GB SSD | + +**Required:** +- βœ… Supabase account ([sign up](https://supabase.com)) +- βœ… Git access +- βœ… Domain (production) + +--- + +## ⚑ Setup + +### 1️⃣ Clone & Install + +```bash +git clone https://github.com/your-org/meshhook.git +cd meshhook && npm install && npm run build +``` + +### 2️⃣ Create Supabase Project + +1. Go to [supabase.com](https://supabase.com) β†’ **New Project** +2. Wait 2-3 minutes for initialization +3. Copy from **Settings β†’ API**: + - `PROJECT_URL` + - `ANON_KEY` + - `SERVICE_ROLE_KEY` + - `DATABASE_URL` + +### 3️⃣ Apply Migrations + +```bash +npx supabase link --project-ref your-project-ref +npx supabase db push +``` + +βœ… **Database ready!** + +--- + +## πŸ”§ Configuration + +### Create `.env` + +```bash +cp .env.example .env +``` + +### Required Variables + +```env +# πŸ”— Application +NODE_ENV=production +PUBLIC_APP_URL=https://your-domain.com +PORT=3000 + +# πŸ—„οΈ Supabase +PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co +PUBLIC_SUPABASE_ANON_KEY=eyJhbGc... +SUPABASE_SERVICE_ROLE_KEY=eyJhbGc... +DATABASE_URL=postgresql://postgres:password@db.xxxxx.supabase.co:5432/postgres + +# πŸ” Security +WEBHOOK_SECRET=generate-random-secret +JWT_SECRET=generate-random-secret +``` + +### Generate Secrets + +```bash +openssl rand -base64 32 # Run twice +``` + +### Optional Variables + +```env +WEBHOOK_TIMEOUT_MS=30000 +WORKER_CONCURRENCY=10 +LOG_LEVEL=info +RATE_LIMIT_ENABLED=true +``` + +--- + +## 🌍 Deploy + +### ⚑ Option 1: Vercel (Fastest) + +```bash +npm install -g vercel +vercel --prod +``` + +Add env vars in Vercel dashboard β†’ Environment Variables + +βœ… Live at `https://your-project.vercel.app` + +--- + +### 🐳 Option 2: Docker + +**`Dockerfile`:** +```dockerfile +FROM node:18-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM node:18-alpine +WORKDIR /app +RUN apk add --no-cache dumb-init +COPY --from=builder /app/build ./build +COPY --from=builder /app/package*.json ./ +RUN npm ci --production +EXPOSE 3000 +ENTRYPOINT ["dumb-init", "--"] +CMD ["node", "build"] +``` + +**`docker-compose.yml`:** +```yaml +version: '3.8' +services: + app: + build: . + ports: ["3000:3000"] + env_file: .env + depends_on: [worker] + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + + worker: + build: . + command: node worker.js + env_file: .env + restart: unless-stopped + + nginx: + image: nginx:alpine + ports: ["80:80", "443:443"] + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./certs:/etc/nginx/certs:ro + depends_on: [app] +``` + +**Deploy:** +```bash +docker-compose up -d +docker-compose logs -f +``` + +βœ… Live at `http://localhost` + +--- + +### πŸ–₯️ Option 3: Linux VPS + +```bash +ssh user@your-server-ip + +# Install Node.js +curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - +sudo apt install -y nodejs git + +# Clone & setup +git clone https://github.com/your-org/meshhook.git /var/www/meshhook +cd /var/www/meshhook +npm install && npm run build + +# Install PM2 +sudo npm install -g pm2 + +# Create ecosystem.config.js +cat > ecosystem.config.js << 'EOF' +module.exports = { + apps: [{ + name: 'meshhook', + script: './build/index.js', + instances: 'max', + exec_mode: 'cluster', + env: { NODE_ENV: 'production' }, + max_memory_restart: '1G' + }, { + name: 'meshhook-worker', + script: './worker.js', + instances: 1, + env: { NODE_ENV: 'production' } + }] +}; +EOF + +# Start +pm2 start ecosystem.config.js +pm2 startup && pm2 save + +# Setup Nginx +sudo apt install -y nginx + +sudo tee /etc/nginx/sites-available/meshhook << 'EOF' +server { + listen 80; + server_name your-domain.com; + location / { + proxy_pass http://localhost:3000; + proxy_set_header Host $host; + } +} +EOF + +sudo ln -s /etc/nginx/sites-available/meshhook /etc/nginx/sites-enabled/ +sudo systemctl restart nginx +``` + +βœ… Live at `http://your-domain.com` + +--- + +### ☸️ Option 4: Kubernetes + +**`k8s/namespace.yaml`:** +```yaml +apiVersion: v1 +kind: Namespace +metadata: + name: meshhook +``` + +**`k8s/secrets.yaml`:** +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: meshhook-secrets + namespace: meshhook +type: Opaque +stringData: + DATABASE_URL: postgresql://postgres:password@db.xxxxx.supabase.co:5432/postgres + SUPABASE_SERVICE_ROLE_KEY: your-key + WEBHOOK_SECRET: your-secret + JWT_SECRET: your-jwt-secret +``` + +**`k8s/deployment.yaml`:** +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: meshhook-app + namespace: meshhook +spec: + replicas: 3 + selector: + matchLabels: + app: meshhook + template: + metadata: + labels: + app: meshhook + spec: + containers: + - name: meshhook + image: your-registry/meshhook:latest + ports: [{containerPort: 3000}] + env: + - name: NODE_ENV + value: "production" + envFrom: + - secretRef: + name: meshhook-secrets + resources: + requests: {memory: "512Mi", cpu: "250m"} + limits: {memory: "1Gi", cpu: "500m"} + livenessProbe: + httpGet: {path: /health, port: 3000} + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: {path: /ready, port: 3000} + initialDelaySeconds: 5 + periodSeconds: 5 + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: meshhook-worker + namespace: meshhook +spec: + replicas: 5 + selector: + matchLabels: + app: meshhook-worker + template: + metadata: + labels: + app: meshhook-worker + spec: + containers: + - name: worker + image: your-registry/meshhook:latest + command: ["node", "worker.js"] + envFrom: + - secretRef: + name: meshhook-secrets + resources: + requests: {memory: "256Mi", cpu: "250m"} + limits: {memory: "512Mi", cpu: "500m"} + +--- +apiVersion: v1 +kind: Service +metadata: + name: meshhook-service + namespace: meshhook +spec: + selector: + app: meshhook + ports: + - port: 80 + targetPort: 3000 + type: LoadBalancer +``` + +**Deploy:** +```bash +kubectl apply -f k8s/namespace.yaml +kubectl create secret generic meshhook-secrets --from-env-file=.env -n meshhook +kubectl apply -f k8s/ +kubectl get pods -n meshhook +``` + +βœ… Live via LoadBalancer IP + +--- + +## πŸ” Security + +### πŸ”’ Enable RLS + +```sql +ALTER TABLE workflows ENABLE ROW LEVEL SECURITY; +ALTER TABLE workflow_runs ENABLE ROW LEVEL SECURITY; +ALTER TABLE workflow_logs ENABLE ROW LEVEL SECURITY; + +CREATE POLICY tenant_isolation ON workflows + FOR ALL USING (tenant_id = auth.uid()); + +CREATE POLICY tenant_isolation_runs ON workflow_runs + FOR ALL USING (workflow_id IN ( + SELECT id FROM workflows WHERE tenant_id = auth.uid() + )); +``` + +### ✍️ Webhook Verification + +```javascript +// src/lib/webhook-security.js +import crypto from 'crypto'; + +export function verifyWebhookSignature(body, signature, secret) { + const expectedSignature = crypto + .createHmac('sha256', secret) + .update(body) + .digest('hex'); + + return crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(expectedSignature) + ); +} + +// Usage +export async function POST({ request }) { + const signature = request.headers.get('X-Webhook-Signature'); + const body = await request.text(); + + if (!verifyWebhookSignature(body, signature, process.env.WEBHOOK_SECRET)) { + return new Response('Invalid', { status: 401 }); + } + // Process... +} +``` + +### πŸ”‘ Secrets Management + +**AWS:** +```bash +aws secretsmanager create-secret --name meshhook/secret --secret-string "value" +``` + +**GCP:** +```bash +echo -n "value" | gcloud secrets create meshhook-secret --data-file=- +``` + +**Azure:** +```bash +az keyvault secret set --vault-name meshhook-vault --name secret --value "value" +``` + +### πŸ” HTTPS/SSL + +**Self-Hosted with Let's Encrypt:** +```bash +sudo apt install -y certbot python3-certbot-nginx +sudo certbot certonly --nginx -d your-domain.com +``` + +**Update Nginx:** +```nginx +server { + listen 443 ssl http2; + server_name your-domain.com; + ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + location / { + proxy_pass http://localhost:3000; + } +} +server { + listen 80; + server_name your-domain.com; + return 301 https://$server_name$request_uri; +} +``` + +### β˜‘οΈ Security Checklist + +- [ ] Env vars configured +- [ ] WEBHOOK_SECRET strong (32+ chars) +- [ ] JWT_SECRET unique +- [ ] HTTPS enabled +- [ ] RLS enabled +- [ ] Service role key protected +- [ ] Rate limiting on + +--- + +## πŸ“Š Monitor + +### πŸ₯ Health Checks + +```javascript +// src/routes/health/+server.js +export async function GET() { + return new Response(JSON.stringify({ status: 'healthy' }), { + headers: { 'Content-Type': 'application/json' } + }); +} + +// src/routes/ready/+server.js +import { supabase } from '$lib/supabase'; + +export async function GET() { + try { + await supabase.from('workflows').select('count').limit(1); + return new Response(JSON.stringify({ status: 'ready' })); + } catch (error) { + return new Response(JSON.stringify({ status: 'not ready' }), { status: 503 }); + } +} +``` + +### πŸ“‘ Real-Time Logs + +```javascript +// src/lib/logging.js +import { createClient } from '@supabase/supabase-js'; + +const supabase = createClient( + process.env.PUBLIC_SUPABASE_URL, + process.env.SUPABASE_SERVICE_ROLE_KEY +); + +export function subscribeToLogs(workflowId, callback) { + return supabase.channel(`logs:${workflowId}`) + .on('postgres_changes', { + event: 'INSERT', + schema: 'public', + table: 'workflow_logs', + filter: `workflow_id=eq.${workflowId}` + }, (payload) => callback(payload.new)) + .subscribe(); +} + +export async function logEvent(workflowId, runId, message, level = 'info') { + await supabase.from('workflow_logs').insert({ + workflow_id: workflowId, + run_id: runId, + message, + level, + created_at: new Date().toISOString() + }); +} +``` + +### 🚨 Error Tracking (Sentry) + +```bash +npm install @sentry/sveltekit +``` + +```javascript +// src/hooks.server.js +import * as Sentry from "@sentry/sveltekit"; + +Sentry.init({ + dsn: process.env.SENTRY_DSN, + tracesSampleRate: 0.1, + environment: process.env.NODE_ENV +}); +``` + +--- + +## πŸ“ˆ Scale + +### ⬆️ Horizontal Scaling + +```bash +# Docker +docker-compose up -d --scale worker=5 + +# Kubernetes +kubectl scale deployment meshhook-app --replicas=10 -n meshhook +kubectl autoscale deployment meshhook-app --min=3 --max=20 --cpu-percent=70 -n meshhook + +# PM2 +pm2 start npm --name "meshhook" -- start -i max +``` + +### πŸ—„οΈ Database Optimization + +```sql +-- Create indexes +CREATE INDEX idx_workflows_tenant_id ON workflows(tenant_id); +CREATE INDEX idx_workflow_runs_workflow_id ON workflow_runs(workflow_id); +CREATE INDEX idx_workflow_runs_status ON workflow_runs(status); +CREATE INDEX idx_workflow_logs_run_id ON workflow_logs(run_id); +CREATE INDEX idx_workflow_logs_created_at ON workflow_logs(created_at DESC); + +-- Composite index +CREATE INDEX idx_runs_tenant_status + ON workflow_runs(tenant_id, status, created_at DESC); + +-- Analyze +EXPLAIN ANALYZE SELECT * FROM workflow_runs WHERE tenant_id = 'xxx'; +``` + +### πŸ”Œ Connection Pooling + +```env +DATABASE_POOL_SIZE=20 +DATABASE_POOL_TIMEOUT=10000 +DATABASE_IDLE_TIMEOUT=30000 +``` + +--- + +## πŸ”„ Updates + +### πŸ“¦ Update Process + +```bash +# 1. Backup +pg_dump $DATABASE_URL > backup-$(date +%Y%m%d).sql + +# 2. Update code +git pull origin main +npm install + +# 3. Migrations +npx supabase db push + +# 4. Build & test +npm run build && npm run test + +# 5. Deploy +# Vercel +vercel --prod + +# Docker +docker-compose down && docker-compose build && docker-compose up -d + +# PM2 +pm2 restart meshhook + +# Kubernetes +kubectl set image deployment/meshhook-app meshhook=your-registry/meshhook:v1.1.0 -n meshhook +``` + +### πŸ”™ Rollback + +```bash +# Git +git checkout HEAD~1 && npm run build + +# PM2 +pm2 restart meshhook + +# Docker +docker-compose down && docker-compose up -d + +# Kubernetes +kubectl rollout undo deployment/meshhook-app -n meshhook + +# Database +pg_restore -h your-db.supabase.co -U postgres -d postgres backup.sql +``` + +--- + +## πŸ› Troubleshoot + +### ❌ Database Connection Failed + +```bash +echo $DATABASE_URL +psql $DATABASE_URL -c "SELECT 1" +telnet db.xxxxx.supabase.co 5432 +``` + +**Check:** Supabase dashboard β†’ Settings β†’ Project Status + +--- + +### ❌ Webhook Signature Fails + +```bash +echo $WEBHOOK_SECRET +curl -X POST http://localhost:3000/api/webhooks/test \ + -H "X-Webhook-Signature: $(echo -n 'test' | openssl dgst -sha256 -hmac $WEBHOOK_SECRET)" \ + -d '{"test":"data"}' +``` + +**Check:** Middleware order (raw body before parsing) + +--- + +### ❌ Workers Not Processing + +```bash +ps aux | grep worker +docker ps | grep worker +kubectl get pods -n meshhook -l app=meshhook-worker + +# Logs +pm2 logs meshhook-worker +docker-compose logs meshhook-worker +kubectl logs -f deployment/meshhook-worker -n meshhook + +# Queue depth +psql $DATABASE_URL -c "SELECT COUNT(*) FROM workflow_runs WHERE status='pending'" + +# Restart +pm2 restart meshhook-worker +docker-compose restart worker +kubectl rollout restart deployment/meshhook-worker -n meshhook + +# Increase in .env +WORKER_CONCURRENCY=20 +``` + +--- + +### ❌ High Memory + +```bash +docker stats +kubectl top pod + +# Increase limits +NODE_OPTIONS="--max-old-space-size=2048" npm start +``` + +**Docker:** `memory: 2G` +**K8s:** `memory: "2Gi"` + +--- + +### ❌ Slow Queries + +```sql +SELECT query, calls, mean_time FROM pg_stat_statements +ORDER BY mean_time DESC LIMIT 10; + +EXPLAIN ANALYZE SELECT * FROM workflow_runs WHERE tenant_id = 'xxx'; + +CREATE INDEX idx_runs_tenant_status ON workflow_runs(tenant_id, status); + +VACUUM ANALYZE; +``` + +--- + +### ❌ SSL Certificate Error + +```bash +openssl s_client -connect your-domain.com:443 -showcerts | grep dates +sudo certbot renew --force-renewal +kubectl create secret tls meshhook-tls --cert=cert.pem --key=key.pem -n meshhook +``` + +--- + +### ❌ No Real-Time Logs + +```javascript +const channel = supabase.channel('test'); +channel.subscribe((status) => console.log(status)); // Should be: SUBSCRIBED +``` + +```sql +ALTER TABLE workflow_logs REPLICA IDENTITY FULL; +CREATE POLICY "Enable realtime" ON workflow_logs FOR SELECT USING (true); +``` + +**Check:** Dashboard β†’ Settings β†’ API β†’ Realtime β†’ ON + +--- + +### ❌ Build Fails + +```bash +rm -rf node_modules .svelte-kit dist build +npm cache clean --force +npm ci +npm run build +npm run typecheck +``` + +--- + +## βœ… Deployment Checklist + +### Before Deploy + +- [ ] Tests passing +- [ ] Env vars documented +- [ ] Migrations tested +- [ ] RLS policies created +- [ ] Indexes added +- [ ] Secrets generated (32+ chars) +- [ ] HTTPS configured +- [ ] CORS defined + +### During Deploy + +- [ ] Database backup created +- [ ] Team notified +- [ ] Health checks pass + +### After Deploy + +- [ ] App running +- [ ] Users accessing +- [ ] Webhooks working +- [ ] Real-time logs ok +- [ ] Performance good +- [ ] Monitoring alerts on + +--- + +## πŸ“š Quick Ref + +### Commands + +```bash +npm run dev # Dev server +npm run build # Build +npm run preview # Preview build +npx supabase db push # Migrate +npx supabase status # Status + +vercel --prod # Deploy to Vercel +docker-compose up -d # Deploy Docker +pm2 restart meshhook # Restart PM2 +kubectl apply -f k8s/ # Deploy K8s +``` + +### Endpoints + +| Path | Purpose | +|------|---------| +| `GET /health` | Health check | +| `GET /ready` | Ready check | +| `GET /api/workflows` | List workflows | +| `POST /api/workflows` | Create workflow | +| `POST /api/webhooks/:id` | Receive webhook | +| `GET /api/workflows/:id/runs` | Get runs | + +--- + +πŸ“– **Docs:** [docs.meshhook.io](https://docs.meshhook.io) +πŸ› **Issues:** [GitHub](https://github.com/your-org/meshhook/issues) +πŸ’¬ **Support:** [Community](https://meshhook.io/community) + +--- + +**πŸŽ‰ You're ready to deploy MeshHook!** \ No newline at end of file diff --git a/apps/web/src/app.css b/apps/web/src/app.css index 4cee0a3..0ed0ff3 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -1,3 +1,5 @@ +@import '/styles/themes.css'; + :root { --color-bg-0: rgb(202, 216, 228); --color-bg-1: hsl(209, 36%, 86%); diff --git a/apps/web/src/lib/components/Header.svelte b/apps/web/src/lib/components/Header.svelte index edaf4d2..322bb7e 100644 --- a/apps/web/src/lib/components/Header.svelte +++ b/apps/web/src/lib/components/Header.svelte @@ -1,5 +1,6 @@ + + + + \ No newline at end of file diff --git a/apps/web/src/lib/stores/themes.js b/apps/web/src/lib/stores/themes.js new file mode 100644 index 0000000..441e118 --- /dev/null +++ b/apps/web/src/lib/stores/themes.js @@ -0,0 +1,57 @@ +// src/lib/stores/theme.js +import { writable } from 'svelte/store'; +import { browser } from '$app/environment'; + +const THEME_KEY = 'meshhook-theme'; + +function getSystemTheme() { + if (!browser) return 'light'; + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +} + +function createThemeStore() { + // Get theme: localStorage > system preference > light + const storedTheme = browser ? localStorage.getItem(THEME_KEY) : null; + const initialTheme = storedTheme || getSystemTheme(); + + const { subscribe, set, update } = writable(initialTheme); + + function applyTheme(theme) { + if (browser) { + document.documentElement.setAttribute('data-theme', theme); + localStorage.setItem(THEME_KEY, theme); + } + } + + if (browser) { + applyTheme(initialTheme); + + // Listen for system theme changes + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + mediaQuery.addEventListener('change', (e) => { + // Only update if user hasn't manually set a preference + if (!localStorage.getItem(THEME_KEY)) { + const newTheme = e.matches ? 'dark' : 'light'; + set(newTheme); + applyTheme(newTheme); + } + }); + } + + return { + subscribe, + toggle: () => { + update(current => { + const newTheme = current === 'light' ? 'dark' : 'light'; + applyTheme(newTheme); + return newTheme; + }); + }, + set: (theme) => { + set(theme); + applyTheme(theme); + } + }; +} + +export const theme = createThemeStore(); \ No newline at end of file diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte index 8c66aae..4918b3e 100644 --- a/apps/web/src/routes/+layout.svelte +++ b/apps/web/src/routes/+layout.svelte @@ -14,5 +14,7 @@ \ No newline at end of file diff --git a/apps/web/src/routes/api/user/theme/+server.js b/apps/web/src/routes/api/user/theme/+server.js new file mode 100644 index 0000000..8a6c168 --- /dev/null +++ b/apps/web/src/routes/api/user/theme/+server.js @@ -0,0 +1,57 @@ +import { json } from '@sveltejs/kit'; +import { createServerSupabaseClient } from '$lib/supabase.js'; + +export async function POST(event) { + const supabase = createServerSupabaseClient(event); + + const { data: { user } } = await supabase.auth.getUser(); + + if (!user) { + return json({ error: 'Unauthorized' }, { status: 401 }); + } + + const { theme } = await event.request.json(); + + if (!theme || !['light', 'dark'].includes(theme)) { + return json({ error: 'Invalid theme' }, { status: 400 }); + } + + const { error } = await supabase + .from('user_settings') + .upsert({ + user_id: user.id, + theme_preference: theme, + updated_at: new Date().toISOString() + }, { + onConflict: 'user_id' + }); + + if (error) { + console.error('Failed to update theme:', error); + return json({ error: 'Failed to update theme' }, { status: 500 }); + } + + return json({ success: true, theme }); +} + +export async function GET(event) { + const supabase = createServerSupabaseClient(event); + + const { data: { user } } = await supabase.auth.getUser(); + + if (!user) { + return json({ theme: 'light' }); + } + + const { data, error } = await supabase + .from('user_settings') + .select('theme_preference') + .eq('user_id', user.id) + .single(); + + if (error || !data) { + return json({ theme: 'light' }); + } + + return json({ theme: data.theme_preference }); +} \ No newline at end of file diff --git a/apps/web/static/styles/themes.css b/apps/web/static/styles/themes.css new file mode 100644 index 0000000..c52231a --- /dev/null +++ b/apps/web/static/styles/themes.css @@ -0,0 +1,175 @@ +/* static/styles/themes.css */ +/* MeshHook Theme System - CSS Custom Properties */ + +/* ============================================ + ROOT - Default Light Theme + ============================================ */ +:root { + /* Primary Colors */ + --color-primary: #3b82f6; + --color-primary-hover: #2563eb; + --color-primary-light: #dbeafe; + --color-primary-dark: #1e40af; + + /* Background Colors */ + --color-bg-primary: #ffffff; + --color-bg-secondary: #f9fafb; + --color-bg-tertiary: #f3f4f6; + --color-bg-hover: #f3f4f6; + --color-bg-active: #e5e7eb; + + /* Text Colors */ + --color-text-primary: #111827; + --color-text-secondary: #6b7280; + --color-text-tertiary: #9ca3af; + --color-text-inverse: #ffffff; + --color-text-muted: #6b7280; + + /* Border Colors */ + --color-border-primary: #e5e7eb; + --color-border-secondary: #d1d5db; + --color-border-focus: #3b82f6; + --color-border-hover: #9ca3af; + + /* Status Colors */ + --color-success: #10b981; + --color-success-bg: #d1fae5; + --color-warning: #f59e0b; + --color-warning-bg: #fef3c7; + --color-error: #ef4444; + --color-error-bg: #fee2e2; + --color-info: #3b82f6; + --color-info-bg: #dbeafe; + + /* Component Specific */ + --color-card-bg: #ffffff; + --color-card-border: #e5e7eb; + --color-card-shadow: rgba(0, 0, 0, 0.1); + + --color-input-bg: #ffffff; + --color-input-border: #d1d5db; + --color-input-focus: #3b82f6; + --color-input-disabled: #f3f4f6; + + --color-button-primary: #3b82f6; + --color-button-primary-hover: #2563eb; + --color-button-secondary: #6b7280; + --color-button-secondary-hover: #4b5563; + + --color-nav-bg: #ffffff; + --color-nav-border: #e5e7eb; + --color-nav-text: #111827; + --color-nav-hover: #f3f4f6; + + --color-sidebar-bg: #f9fafb; + --color-sidebar-border: #e5e7eb; + --color-sidebar-text: #374151; + --color-sidebar-hover: #e5e7eb; + --color-sidebar-active: #dbeafe; + + --color-code-bg: #f3f4f6; + --color-code-text: #1f2937; + + /* Shadows */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1); + + /* Transitions */ + --transition-fast: 150ms ease-in-out; + --transition-normal: 250ms ease-in-out; + --transition-slow: 350ms ease-in-out; +} + +/* ============================================ + DARK THEME + ============================================ */ +[data-theme="dark"] { + /* Primary Colors */ + --color-primary: #60a5fa; + --color-primary-hover: #3b82f6; + --color-primary-light: #1e3a8a; + --color-primary-dark: #93c5fd; + + /* Background Colors */ + --color-bg-primary: #111827; + --color-bg-secondary: #1f2937; + --color-bg-tertiary: #374151; + --color-bg-hover: #374151; + --color-bg-active: #4b5563; + + /* Text Colors */ + --color-text-primary: #f9fafb; + --color-text-secondary: #d1d5db; + --color-text-tertiary: #9ca3af; + --color-text-inverse: #111827; + --color-text-muted: #9ca3af; + + /* Border Colors */ + --color-border-primary: #374151; + --color-border-secondary: #4b5563; + --color-border-focus: #60a5fa; + --color-border-hover: #6b7280; + + /* Status Colors */ + --color-success: #34d399; + --color-success-bg: #064e3b; + --color-warning: #fbbf24; + --color-warning-bg: #78350f; + --color-error: #f87171; + --color-error-bg: #7f1d1d; + --color-info: #60a5fa; + --color-info-bg: #1e3a8a; + + /* Component Specific */ + --color-card-bg: #1f2937; + --color-card-border: #374151; + --color-card-shadow: rgba(0, 0, 0, 0.3); + + --color-input-bg: #1f2937; + --color-input-border: #4b5563; + --color-input-focus: #60a5fa; + --color-input-disabled: #374151; + + --color-button-primary: #60a5fa; + --color-button-primary-hover: #3b82f6; + --color-button-secondary: #6b7280; + --color-button-secondary-hover: #9ca3af; + + --color-nav-bg: #1f2937; + --color-nav-border: #374151; + --color-nav-text: #f9fafb; + --color-nav-hover: #374151; + + --color-sidebar-bg: #111827; + --color-sidebar-border: #374151; + --color-sidebar-text: #d1d5db; + --color-sidebar-hover: #374151; + --color-sidebar-active: #1e3a8a; + + --color-code-bg: #0f172a; + --color-code-text: #e2e8f0; + + /* Shadows (darker in dark mode) */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.6); +} + +/* ============================================ + BASE STYLES - Apply theme colors + ============================================ */ +body { + background-color: var(--color-bg-primary); + color: var(--color-text-primary); + transition: background-color var(--transition-normal), color var(--transition-normal); +} + +/* Smooth theme transitions for all themed elements */ +* { + transition: background-color var(--transition-fast), + border-color var(--transition-fast), + color var(--transition-fast); +} \ No newline at end of file diff --git a/supabase/migrations/20250111000008_add_theme_preference.sql b/supabase/migrations/20250111000008_add_theme_preference.sql new file mode 100644 index 0000000..0da7041 --- /dev/null +++ b/supabase/migrations/20250111000008_add_theme_preference.sql @@ -0,0 +1,49 @@ +-- Add theme_preference to user_settings table +-- Create user_settings table if it doesn't exist +CREATE TABLE IF NOT EXISTS user_settings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE UNIQUE NOT NULL, + theme_preference TEXT DEFAULT 'light' CHECK (theme_preference IN ('light', 'dark')), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Add theme_preference column if table already exists but column doesn't +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'user_settings' AND column_name = 'theme_preference' + ) THEN + ALTER TABLE user_settings + ADD COLUMN theme_preference TEXT DEFAULT 'light' + CHECK (theme_preference IN ('light', 'dark')); + END IF; +END $$; + +-- Create indexes for faster queries +CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id); +CREATE INDEX IF NOT EXISTS idx_user_settings_theme ON user_settings(theme_preference); + +-- Enable Row Level Security +ALTER TABLE user_settings ENABLE ROW LEVEL SECURITY; + +-- Drop existing policies if they exist +DROP POLICY IF EXISTS "Users can read own settings" ON user_settings; +DROP POLICY IF EXISTS "Users can insert own settings" ON user_settings; +DROP POLICY IF EXISTS "Users can update own settings" ON user_settings; + +-- Policy: Users can read their own settings +CREATE POLICY "Users can read own settings" + ON user_settings FOR SELECT + USING (auth.uid() = user_id); + +-- Policy: Users can insert their own settings +CREATE POLICY "Users can insert own settings" + ON user_settings FOR INSERT + WITH CHECK (auth.uid() = user_id); + +-- Policy: Users can update their own settings +CREATE POLICY "Users can update own settings" + ON user_settings FOR UPDATE + USING (auth.uid() = user_id); \ No newline at end of file