# Hub / Dashboard Architecture Map Wiring for Anthony's HTML hubs served over Tailscale via Caddy. Read this before touching any hub, Caddyfile, renderer, or data cron. Verified 2026-06-25 against the live Caddyfile + launchd plists. ## Shared architecture - **Host:** all hubs live under one Tailscale host `https://ironman-2.tail070205.ts.net/` (Funnel = publicly reachable, so basic_auth is load-bearing security, not optional). - **Web server:** single Caddy instance, `:8080`, config `~/Documents/Claude/html-share/Caddyfile`, launchd `com.claude.caddy`. Reload: `launchctl kickstart -k gui/$UID/com.claude.caddy` (this host app is TCC-blocked from reading the Caddyfile; the daemon isn't). `caddy hash-password --plaintext ''` to change a password. - **Build pattern (shell vs render split):** a hub is a static `index.html` shell that iframes feature pages by stable URL (`/latest.html` or `index.html`). Briefing/content changes go in the renderer (`~/.claude/skills/today/render.py` for Work/Family Today); shell/nav changes in the static `index.html`. CST/HR/Finance dashboards are emitted by their own Python renderers. - **Left-nav redesign:** full-height left sidebar, collapsed by default (localStorage), icon-rail on phones. CST/HR get it via the shared `report-widget.js` runtime transform (loaded on every page → all current + future generated pages inherit it). See `~/.claude/html-stack.md` "Hub & dashboard layout". - **Shared assets:** `report-widget.js` (🐛 report button + sidebar transform + top-bar quote) lives in each dashboard's App Support dir, served standalone (no deploy copy). Every hub fetches ONE canonical quote at `/work-hub/today/quote.json`. - **Auth model:** per-surface Caddy `basic_auth` matchers. Realm string = browser credential namespace. Work Hub, CST, HR, Finance all share realm **"Work Hub" + the same `lim` bcrypt hash** on purpose, so the cached cred carries across iframed sub-hubs without a second prompt. Family Hub + Swing use realm **"Family Hub"** (own `lim` hash, shared with wife). Sub-hubs are aliased UNDER `/work-hub//*` (strip_prefix) — never iframe a sibling basic-auth path (frames render the 401 as a blank tab). - **Data location:** launchd crons are TCC-blocked from `~/Documents`. All cron-written data + launchd-rendered pages go to `~/Library/Application Support//`, served by a Caddy `file_server` rooted there. ## Per-hub map | Hub | URL path | Files / data root | Auth users (role) | |---|---|---|---| | Work Hub (Today) | `/work-hub/*` | shell in `html-share/work-hub/`; Today iframe + data in App Support `work-hub/today/` | `lim` (Anthony) | | Family Hub (PWA) | `/family-hub/*`, `/family-money*` | shell `html-share/family-hub/`; JSON + today in App Support `family-hub/data/` & `/today/` | `lim` (Anthony + wife Yessi) | | CST / Pam dashboard | `/cst-dashboard/*`, aliased `/work-hub/cst/*` | App Support `cst-dashboard/` (renderer-written) | `lim`, `ross`, `ardie` | | HR dashboard | `/hr-dashboard/*`, aliased `/work-hub/hr/*` | App Support `hr-dashboard/` | `lim` | | Finance dashboard | `/finance*` | static `html-share/finance/` (live App-Support hub not yet built; blocked on data feed) | `lim` | ## Data crons (launchd, verified) | Label | Schedule | Writes | Where | |---|---|---|---| | `com.claude.hub-data` | 6/9/12/15/18/21 | Family Hub JSON (home/health/etc.) | App Support `family-hub/data/` | | `com.claude.today-briefing` | 5:30 AM | Work Hub Today briefing | App Support `work-hub/today/` | | `com.claude.hub-freshness-guard` | 8:00 + 10:30 AM | re-renders/republishes a stale hub | App Support | | `com.claude.hub-attention` | 6:30 AM | "Needs Attention" brief | vault inbox | | `com.claude.cst-dashboard-regen` | every 15 min | CST tabs | App Support `cst-dashboard/` | | `com.claude.hr-dashboard-regen` | 6:35 AM | HR tabs (skips hand-authored pages) | App Support `hr-dashboard/` | | `com.claude.finance-scorecard` | 6:15 AM | AP scorecard data | `cron-launchers/finance-scorecard.sh` | ## Gotchas (these silently break a hub) 1. **TCC write location.** launchd crons CANNOT write `~/Documents`. New cron-fed data/pages MUST go to `~/Library/Application Support//` with a matching Caddy `file_server` root. Inside launchd, coreutils (cp/ln/grep) may also be TCC-denied on ~/Documents — do that I/O in `python3` (FDA-independent). Never use symlinks for `latest.html`; `cp` the dated file (symlink syscalls EPERM in sandbox). 2. **quote.json auth exemption.** `report-widget.js` fetches `/work-hub/today/quote.json` (a `lim`-only path) on EVERY hub. Without the `@workhub not path …/quote.json` exemption, non-`lim` users (Ross/Ardie/team) get a repeating 401 login loop. The exemption line is already in the Caddyfile — keep it; the file is non-sensitive so public is fine. 3. **Caddy handle order is NOT preserved.** The caddyfile adapter reorders `handle` blocks, so a catch-all `file_server` can sort above an API route and 404 it. Every `@*files` file_server matcher MUST list explicit `not path` exclusions for its API subpaths. Never rely on source order. 4. **Service-worker cache bump.** Family Hub registers `family-hub/sw.js` (network-first). On ANY shell edit (index.html/sw.js/icons), bump its `CACHE` version, or phones serve the stale shell. 5. **Absolute write-back paths.** A hub page POSTing a RELATIVE endpoint breaks when served under an alias (e.g. `/work-hub/hr/`) — writer routes are only wired under the canonical `/hr-dashboard/` (etc.). Always POST absolute canonical endpoints. Diagnose save failures from `html-share/access.log` (real URL+status), not theory.