# 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 ```bash pip install radicale bcrypt ``` Create config at `~/.config/radicale/config`: ```ini [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): ```bash 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`: ```ini [all] user: .+ collection: .* permissions: RrWw ``` ### Install Systemd Service (requires sudo) ```bash 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 ```bash curl -s -u assistant:YOUR_PASSWORD -o /dev/null -w "%{http_code}" http://127.0.0.1:5232/ # Should return 302 ``` ### Create Calendar ```python 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.com` → `localhost:5000` (webhook) 2. `cal.yourdomain.com` → `localhost:5232` (Radicale) In the Radicale config, add the header: ```ini [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) ```bash 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 ```bash curl http://127.0.0.1:5000/health # {"status":"healthy"} ``` --- # 4. Update Cloudflare Email Worker > **Already deployed as `hoffdesk-email`** — this is a quick reference. ### To update: 1. Go to Cloudflare Dashboard → **Workers & Pages** → **hoffdesk-email** 2. Edit code, paste contents of `scripts/email_worker.js` (v3, canonical) 3. **Settings** → **Variables** → Ensure `WEBHOOK_SECRET` is set to match `.env` 4. Deploy ### Worker Details - **Name:** `hoffdesk-email` - **Trigger:** `assistant@hoffdesk.com` via Email Routing - **Webhook URL:** `https://hook.hoffdesk.com/webhook` - **Secret:** `WEBHOOK_SECRET` env var ### Quick test from Beelink: ```bash curl -X POST https://hook.hoffdesk.com/webhook \ -H "Content-Type: application/json" \ -H "X-Hoffdesk-Secret: your-webhook-secret" \ -d '{"from":"test@example.com","to":"assistant@hoffdesk.com","subject":"Test","date":"$(date -u +'%a, %d %b %Y %H:%M:%S UTC')","message_id":"","raw_email":"test"}' ``` --- ## 5. Environment Variables All secrets in `scripts/.env` (chmod 600, gitignored): ```bash # 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 ```bash # 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: ```bash REMOTE_HOST=user@remote-host.tail-xxxxx.ts.net ``` --- ## End-to-End Test ```bash # 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.