📄 WEBHOOK_DEPLOY.md 7,134 bytes Apr 19, 2026 📋 Raw

Family Assistant — Deployment Instructions

Architecture Overview

Email → Cloudflare → Worker JS → hook.yourdomain.com → FastAPI (localhost:5000) → pipeline → Radicale CalDAV (localhost:5232) → Phone sync
                                                                                                          → ChromaDB (Brain)
                                                                                                          → Hermes (Telegram)

1. Radicale CalDAV Server

Install & Configure

pip install radicale bcrypt

Create config at ~/.config/radicale/config:

[server]
hosts = 0.0.0.0:5232

[auth]
type = htpasswd
htpasswd_filename = ~/.config/radicale/htpasswd
htpasswd_encryption = bcrypt

[rights]
type = from_file
file = ~/.config/radicale/rights

[storage]
filesystem_folder = ~/.local/share/radicale/collections

Create htpasswd (use bcrypt — $2b$ prefix required, $apr1$ will fail):

htpasswd -B -c ~/.config/radicale/htpasswd assistant
# Add more users:
htpasswd -B ~/.config/radicale/htpasswd matt
htpasswd -B ~/.config/radicale/htpasswd aundrea

Create rights file at ~/.config/radicale/rights:

[all]
user: .+
collection: .*
permissions: RrWw

Install Systemd Service (requires sudo)

sudo ln -sf /path/to/family-assistant/systemd/radicale.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable radicale
sudo systemctl start radicale

Verify

curl -s -u assistant:YOUR_PASSWORD -o /dev/null -w "%{http_code}" http://127.0.0.1:5232/
# Should return 302

Create Calendar

import caldav
client = caldav.DAVClient(url="http://127.0.0.1:5232", username="assistant", password="YOUR_PASSWORD")
client.principal().make_calendar(name="family")

Phone Setup (via Cloudflare Tunnel)

Add CalDAV account on iPhone:
- Server: cal.yourdomain.com (Cloudflare Tunnel → localhost:5232)
- Username: assistant (shared account for family visibility)
- Password: your Radicale password
- Description: Family Calendar

Note: iOS requires HTTPS for CalDAV. Use a Cloudflare Tunnel or reverse proxy with TLS.

iOS Discovery: iOS only auto-discovers calendars under the logged-in user's principal. Both family members should use the assistant account for shared visibility.


2. Cloudflare Tunnel

Set up two tunnels (or two public hostnames on the same tunnel):

  1. hook.yourdomain.comlocalhost:5000 (webhook)
  2. cal.yourdomain.comlocalhost:5232 (Radicale)

In the Radicale config, add the header:

[headers]
X-Forwarded-Proto = https

This tells Radicale to generate proper HTTPS URLs in CalDAV responses.


3. Webhook Server (FastAPI)

Install Systemd Service (requires sudo)

sudo ln -sf /path/to/family-assistant/systemd/hoffdesk-webhook.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable hoffdesk-webhook.service
sudo systemctl start hoffdesk-webhook.service

Verify

curl http://127.0.0.1:5000/health
# {"status":"healthy"}

4. Cloudflare Email Worker

  1. Go to Cloudflare Dashboard → Workers & PagesCreate Worker
  2. Name it email-assistant
  3. Paste contents of scripts/email_worker.js
  4. SettingsVariables and Secrets → Add:
    - WEBHOOK_SECRET: (generate with openssl rand -base64 48, match WEBHOOK_SECRET in .env)
  5. Deploy

Configure Email Routing

  1. Cloudflare Dashboard → select your domain → EmailEmail Routing
  2. Add rule: assistant@yourdomain.comSend to Worker
  3. Select the email-assistant worker
  4. Save

Important: The worker code hardcodes the webhook URL (https://hook.yourdomain.com/webhook). Only WEBHOOK_SECRET needs to be set as an env var.


5. Environment Variables

All secrets in scripts/.env (chmod 600, gitignored):

# CalDAV
CALDAV_URL=http://127.0.0.1:5232
CALDAV_USER=assistant
CALDAV_PASSWORD=your-radicale-password
CALDAV_CALENDAR_NAME=family

# Webhook
WEBHOOK_SECRET=your-webhook-secret-here

# LLM
LLM_URL=http://localhost:11434/v1/chat/completions
LLM_MODEL=qwen2.5-coder:7b

# Telegram
TELEGRAM_BOT_TOKEN=your-bot-token
TELEGRAM_CHAT_ID=your-group-chat-id
TELEGRAM_DEV_ID=your-dm-chat-id

# Optional
OLLAMA_EMBED_URL=http://localhost:11434/api/embeddings
VISION_LLM_URL=http://localhost:11434/api/chat
GOOGLE_PLACES_API_KEY=your-key  # Optional, Nominatim is free fallback

6. Backup

# Manual backup
./scripts/backup_hoffdesk.sh

# Skip remote copy (e.g., Gaming PC offline)
SKIP_REMOTE=true ./scripts/backup_hoffdesk.sh

# Runs daily at 7 AM via cron

Backs up: Radicale collections, ChromaDB, location cache, .env files, Radicale config.
Retention: 7 days local, 7 days remote (via Tailscale SSH).

Set REMOTE_HOST in the script or .env to enable remote backup:

REMOTE_HOST=user@remote-host.tail-xxxxx.ts.net

End-to-End Test

# 1. Verify Radicale
python3 -c "from family_assistant.calendar_sync import list_upcoming_events; print(list_upcoming_events())"

# 2. Test webhook pipeline
python3 -c "
from family_assistant.pipeline import process_webhook_email
result = process_webhook_email({
    'from': 'test@test.com', 'to': 'assistant@yourdomain.com',
    'subject': 'Test Appt', 'date': '2026-04-20T10:00:00-05:00',
    'body_text': 'Test on April 21 2026 at 3pm at Home',
    'dedup_key': 'e2e-test', 'message_id': 'e2e-test', 'source': 'webhook'
}, dry_run=True, notify=False)
print(result)
"

# 3. Send real email to assistant@yourdomain.com → verify event appears in Radicale

Troubleshooting

Radicale 401/403: Check htpasswd uses bcrypt ($2b$ prefix), not apr1 ($apr1$). This is the most common Radicale auth failure.

Radicale EADDRNOTAVAIL: Tailscale IPs are virtual interfaces that may rotate. Bind to 0.0.0.0:5232 instead of a specific Tailscale IP.

Webhook 401: Verify X-Hoffdesk-Secret header matches WEBHOOK_SECRET in .env. The worker sends the env var value; the webhook checks the same value from .env.

Calendar not found: CalDAV calendar is auto-discovered by name under the authenticated user. If missing, recreate it manually (see step 1).

iOS CalDAV: iOS requires HTTPS. Use a Cloudflare Tunnel. Both family members should log in as the assistant user for shared calendar visibility — iOS auto-discovery is per-user only.

Webhook not receiving email: Check Cloudflare Email Routing is configured for assistant@yourdomain.com → Worker. Check the worker logs in Cloudflare Dashboard → Workers → Logs.

Location resolution failing: If Google Places API key is missing or suspended, the pipeline falls back to Nominatim (free, no API key). Check location_cache.py logs for resolution attempts.

Day-of-week mismatch warnings: This is intentional — if an email says "Monday the 21st" but the 21st is actually Tuesday, the pipeline adds a ⚠️ to the notification. Check the email source for errors.