# HoffDesk Architecture — Sprint Planning Brief **Date:** 2026-04-24 **Owner:** Matt (Director) **Architects:** Daedalus 🎨 (Frontend/UX), Socrates 🧠 (Backend/Infra) **Model:** OpenClaw agents on Beelink (titanium-butler), Gaming PC (Tailscale) for local LLM --- ## 1. Service Topology A single Beelink machine running everything behind Cloudflare Tunnel. ``` Internet │ ▼ Cloudflare (hoffdesk.com) │ ▼ Cloudflare Tunnel (cloudflared) │ ├── :5232 → Radicale (CalDAV server) │ └── :8000 → hoffdesk-api (FastAPI — single app serving everything) │ ├── Public blog pages (HTML) ├── Blog admin panel (HTML + HTMX) ├── Content generation API (JSON) ├── Family dashboard API (JSON) ├── Family admin (HTML) ├── Auth endpoints (login/logout/session) ├── Webhook receivers └── Telegram bot callback ``` **Key constraint:** One FastAPI process on port 8000 handles all routes. Subdomain separation is cosmetic — the app routes internally. --- ## 2. Subdomain Map | Subdomain | Status | Backend Port | What It Serves | Owner | |-----------|--------|-------------|----------------|-------| | `hoffdesk.com` | ❌ Not configured | — | Public landing page (future) | — | | `notes.hoffdesk.com` | ✅ Live | :8000 | Blog public pages + blog admin + content pipeline | Socrates | | `blog.hoffdesk.com` | ⚠️ No DNS CNAME | :8000 | Duplicate of notes. No purpose. | Remove or redirect | | `api.hoffdesk.com` | ✅ Live | :8000 | JSON API (blog, content, family, health) + blog HTML | Socrates | | `family.hoffdesk.com` | ❌ Not in tunnel | :8000 | Family dashboard (next sprint) | Daedalus + Socrates | | `cal.hoffdesk.com` | ✅ Live | :5232 | CalDAV (Radicale) | Socrates | | `hook.hoffdesk.com` | ✅ Live | :8000 | Webhook receiver (Cloudflare Worker → Beelink) | Socrates | | `proto.hoffdesk.com` | ⚠️ Dev only | :8765 | Dashboard prototype (mock data, manual start) | Retire after family. goes live | ### 🔧 Housekeeping Items - **`blog.hoffdesk.com`** — either add DNS CNAME pointing to the tunnel or remove from `~/.cloudflared/config.yml`. Currently a dead config entry. - **`proto.hoffdesk.com`** — retire once `family.hoffdesk.com` is wired. The dev server needs manual startup and returns mock data; the real API has session auth and actual data now. - **`api.hoffdesk.com`** — currently serves blog HTML at `/` alongside JSON. If we want a clean JSON-only gateway, route by subdomain header. If not, update the plan to match reality. --- ## 3. Route Hierarchy ``` hoffdesk-api ├── [PUBLIC] / │ └── Blog index, article pages, categories, tags │ ├── [PUBLIC] /api/blog/ │ └── JSON API: posts, categories, tags, RSS, sitemap │ ├── [AUTH] /admin/blog/ │ ├── Dashboard, post list, editor, images │ └── Content pipeline (Magic Wand + Struggle v2) │ ├── [AUTH] /api/v1/content/ │ └── Content pipeline v2 API (briefs CRUD, submit, approve, reject, output, score) │ ├── [AUTH] /admin/family/ │ └── Family pipeline status, email webhook config │ ├── [AUTH] /family/events/removed │ └── Recently removed events (machine-to-machine via Bearer token) │ ├── [AUTH] /auth/login, /auth/logout, /auth/me │ └── Session auth (POST login sets cookie, GET me validates) │ ├── [INTERNAL] /health │ └── Health check │ ├── [INTERNAL] /telegram/callback │ └── Telegram bot webhook │ └── [INTERNAL] /webhook └── Generic webhook receiver ``` --- ## 4. Auth Model (Finalized 2026-04-24) **Session cookies for humans, Bearer tokens for machines. No exceptions.** | Route | Auth Method | Who | Notes | |-------|-------------|-----|-------| | `/` and `/api/blog/*` (public) | None | Anyone | SEO + RSS readers | | `/admin/blog/*` | Session cookie | Matt (admin, editor) | Redirects to login if unauthenticated | | `/admin/family/*` | Session cookie | Matt, Aundrea (family) | Role check required | | `/api/v1/content/*` | Session cookie (browser) or Bearer (API) | Matt | Dual-mode for admin panel + scripts | | `/family/events/removed` | Bearer token (`X-Hoffdesk-Secret`) | Dashboard proxy | Machine-to-machine only | | `/telegram/callback` | Internal secret | Telegram bot | Unchanged | | `/webhook` | Internal secret | Cloudflare Worker | Unchanged | ### Credentials | User | Password | Role | Access | |------|----------|------|--------| | `matt` | `hoffdesk-matt-2026` | admin, editor | Everything | | `aundrea` | `hoffdesk-aundrea-2026` | family | Dashboard only | ### Auth Flow (Aundrea) ``` family.hoffdesk.com ↓ (no session cookie) /auth/login?redirect=/family/dashboard/ ↓ (enters credentials) Session cookie set (HttpOnly, SameSite=Lax, 30 days) ↓ Redirect to /family/dashboard/ ↓ HTMX polls /api/today (cookie automatic) ``` --- ## 5. What Each Person Owns ### Socrates 🧠 — Backend & Infrastructure | Component | Status | Next Action | |-----------|--------|-------------| | FastAPI app (port 8000) | ✅ Live | — | | Blog API (13 routes + 36 tests) | ✅ Live | — | | Content pipeline v2 API (8 endpoints) | ✅ Live | Wire Phase 2 frontend templates | | Session auth middleware + login/logout | ✅ Live | — | | `/auth/login` + `/auth/me` + `/auth/logout` | ✅ Live | — | | Role-based access (admin vs family) | ✅ Live | (Verify family role scoping) | | `/api/today` endpoint | ❌ Not built | Sprint priority — real data from Radicale + weather API | | `/family/events/removed` | ✅ Live | Tested and returning data | | `family.hoffdesk.com` tunnel entry | ❌ Not configured | Add to `~/.cloudflared/config.yml` | | Pipeline admin UI routes | ❌ Not wired | Serve Daedalus' Phase 2 templates | ### Daedalus 🎨 — Frontend & UX | Component | Status | Next Action | |-----------|--------|-------------| | Blog templates (7 Jinja2 files + CSS) | ✅ Live | — | | Content pipeline v2 frontend (4 templates + CSS) | ✅ Delivered | Blocked on route wiring | | Auth migration (removed all tokens from templates) | ✅ Done | — | | Family dashboard HTML + CSS | ✅ Delivered | Blocked on `/api/today` + tunnel | | Recently removed events widget (dashboard card) | ✅ Delivered | Hitting live endpoint | | Dashboard login flow | ❌ Not built | Need to add guest-role experience | | Dashboard Jinja2 conversion (for shared layout) | 🔜 Next | After Socrates wires routes | --- ## 6. Sprint Priorities ### Now (unblocked) 1. **Wire family.hoffdesk.com** — add tunnel entry + DNS CNAME. Dashboard goes live. 2. **Wire pipeline Phase 2 templates** to backend routes. Content generation pipeline goes live. 3. **Clean up** `blog.hoffdesk.com` + `proto.hoffdesk.com`. ### This Sprint 4. **Build `/api/today`** — aggregate endpoint returning real calendar + weather + system health. Dashboard becomes real. 5. **Convert dashboard to Jinja2** — shared base layout, auth-aware nav, user greeting. 6. **Build family dashboard login page** — guest-friendly version of the admin login (Aundrea's first impression). ### Next Sprint 7. **Dashboard Week View** — Family Assistant v1.5 UI mocks (conflict panel, undo toast, digest card). 8. **Public landing page** at `hoffdesk.com`. 9. **API gateway pattern** — if we want `api.hoffdesk.com` JSON-only. --- ## 7. Architecture Decisions Log | Date | Decision | Made By | |------|----------|---------| | 2026-04-19 | Sovereign stack: FastAPI + HTMX + Tailwind, no SPA frameworks | Matt | | 2026-04-19 | Mobile-first, dark mode default, no toggle | Matt | | 2026-04-20 | Blog at `notes.hoffdesk.com`, admin at `/admin/blog/` | Matt | | 2026-04-21 | Dashboard at `family.hoffdesk.com`, bare domain reserved for landing | Matt | | 2026-04-24 | Session cookies for browsers, Bearer tokens for API/webhooks | Matt | | 2026-04-24 | Roles: `admin` (full), `family` (dashboard only) | Matt | --- ## 8. Pain Points 1. **Monolith pressure.** One FastAPI app handles blog, admin, content pipeline, family dashboard, webhooks, and bot callbacks. It works now but won't scale to multi-service gracefully. Not urgent, but worth noting. 2. **Dashboard / blog / family admin all live in different template directories.** My workspace templates, shared template symlinks, production template paths — it's easy to update the wrong copy. Need a single source of truth. 3. **Three auth patterns existed for a week.** We killed the tokens-in-URLs pattern, but the session middleware is brand new. Need a sprint of just watching auth logs to catch edge cases. 4. **Dashboard is HTML+JS, not Jinja2.** This means it doesn't inherit shared layouts or benefit from server-side rendering. Converting it matters most when Aundrea starts using it daily.