← Command Centre · In-Flight

Dashboard Push-Live Inbox E2E LIVE v5 → Phase 7 cutover

Replace manual JSON-export-and-drag-into-chat with a Push Live button on every dashboard. End-to-end loop closed 2026-05-10 22:44 BST. Transport cut over to Cloudflare Tunnel 2026-05-17 (Phase 7) — new public hostname inbox.nutritionalproducts.org/jobs + x-api-token auth. Original test (kept for historical record): POST /inbox-api/jobs → HTTP 200 → client_ip=100.100.216.69 source=tailnet → file landed in F:/Agentic-OS/inbox/creative-review/. Session 25 (original) + Session 49B (Phase 7).

📝 Architecture changed 2026-05-17 (Phase 7)

This plan page is the historical record of the v5 design (Coolify Traefik → private Tailnet → FastAPI). The current production architecture is the Phase 7 cutover: browser POSTs cross-origin to inbox.nutritionalproducts.org/jobs (Cloudflare Tunnel) with x-api-token → cloudflared Windows service → FastAPI on localhost:7392. Same FastAPI server, same envelope contract, same Claude inbox folder — just a different public ingress (2 hops instead of 4, removed the 502'ing proxy chain).

Current SOPs: Inbox over Cloudflare Tunnel (Phase 7 rebuild recipe) · Original Push Live Inbox SOP (Phases 1-3 + 6-8 still apply unchanged) · Dashboard SOP — Build a New Dashboard v2.1 at F:/Agentic-OS/reference/it-sops/dashboard-sop.md (canonical for new dashboards — markdown only; triggers: "Build me a new dashboard" / "dashboard SOP").

What changes

Today, every Command Centre dashboard already exports a self-describing JSON envelope (the contract banked in session 25 / commit 5a35da1, documented at feedback_self_describing_exports.md). Today's flow:

Dashboard → click "Export JSON" → download → drag into Claude chat
→ Claude reads _instructions_for_claude → runs the listed command → done

The new flow keeps the envelope unchanged. Only the handoff step is automated:

Dashboard → click "Push Live" → POST same envelope to local server (via Coolify proxy + Tailnet)
→ Server writes to F:/Agentic-OS/inbox/<dashboard>/<timestamp>.json
→ Claude (next active session) reads inbox → processes via _instructions_for_claude → archives
The envelope is the protocol. There is no new op-type registry, no handler dispatcher, no protocol versioning. Every dashboard's existing export tool is the handler. Claude is the dispatcher.

Architecture

Browser (anywhere — basic-auth gated by Coolify) ↓ ↓ HTTPS POST /inbox-api/jobs (envelope as body) ↓ Coolify VPS srv843884 · 100.100.216.69 (already on Tailnet) ↓ ↓ reverse-proxy /inbox-api/* → http://100.127.50.113:7392/inbox/* ↓ routes over private Tailnet, NOT public internet ↓ ai-workstation-gf · 100.127.50.113 · this PC ↓ ↓ FastAPI on port 7392, Tailscale-interface only ↓ Source-IP allowlist (100.x.y.z — Tailnet CGNAT range) ↓ ↓ Writes body to F:/Agentic-OS/inbox/<dashboard>/<timestamp>.json ↓ Returns 200 immediately ↓ Claude (next active session) ↓ ↓ Scans F:/Agentic-OS/inbox/ on startup ↓ For each file: validate _format, run _commands[0] or _instructions_for_claude ↓ Archive to inbox/_processed/<dashboard>/<timestamp>.json on success

How we got to v5 (rejected iterations)

IterApproachWhy rejected
v0Today's flow — manual export, drag into chatWorks but every bank requires user friction.
v1CF Worker + KV with 60-second polling on local PCToo much infra. Needs new KV namespace, Worker code, polling daemon. Latency.
v2CF Worker + Tailscale push to localWorker still adds maintenance overhead. Tailscale alone solves reachability.
v3Tailscale Funnel direct — expose local PC publicly via tailnet edgePublic exposure of local machine. Unnecessary because Coolify VPS is already on Tailnet.
v4Coolify proxy + FastAPI server with per-op-type handler dispatcher + universal client module across 7 dashboardsCalum: "you're overcomplicating it because for every dashboard there was just a simple JSON export and that's all you need to do". The envelope IS the protocol — no handler registry needed.
v5Coolify proxy + FastAPI single endpoint that writes JSON to inbox folder; Claude consumes the existing envelope when next activeLOCKED. Smallest possible change to ship the desired UX. ~7 files, mostly small.

Files in scope

FileActionApprox. sizePurpose
F:/Agentic-OS/tools/inbox_server.pyNEW~60 linesFastAPI single endpoint POST /inbox/jobs. Bind to Tailscale interface only. Source-IP allowlist (Tailnet CGNAT). Write body to disk.
F:/Agentic-OS/tools/setup_inbox_server_task.batNEW~20 linesTask Scheduler entry — start at boot, restart on crash.
F:/brainzyme-git/_shared/inbox-client.jsNEW~30 linesTiny shared module: pushToInbox(envelope) helper. Imported by every dashboard.
Each dashboard's index.htmlMODIFY+5 lines eachAdd or rename "Push Live" button. Swap downloadJson() handler for pushToInbox(). Identical JSON payload — nothing else changes.
Coolify reverse-proxy (Traefik labels)MODIFY2 labelsAdd route /inbox-api/*http://100.127.50.113:7392/inbox/*.
F:/Agentic-OS/reference/services/tailscale.mdNEW~80 linesService contract per Connections Registry rule.
F:/Agentic-OS/reference/connections-registry.mdMODIFY+1 rowNew row for Tailscale.
CLAUDE.mdMODIFY+5 linesStartup hook: scan F:/Agentic-OS/inbox/ on session start; process pending files via _instructions_for_claude.

Behaviour locked

What this does NOT add

Cross-references

Build log — 2026-05-10

PhaseStatusDetail
1. tools/inbox_server.pyDONEFastAPI single-endpoint, source-IP allowlist (Tailnet CGNAT 100.64.0.0/10 + loopback), envelope validation, dashboard slug whitelist, writes to F:/Agentic-OS/inbox/<dashboard>/<timestamp>.json. Smoke-tested locally: POST /inbox/jobs with smoke envelope → HTTP 200 → file landed at inbox/_test/20260510T204622Z_brainzyme_smoke_test_export.json.
2. tools/setup_inbox_server_task.batDONETask Scheduler entry: BrainzymeDashboardInbox, schedule ONLOGON, RL HIGHEST. User to run once (not run yet) to register the boot-startup. Logs to F:/Agentic-OS/logs/inbox_server_task.log.
3. _shared/inbox-client.jsDONEThree exports: pushToInbox(envelope, opts) · pushAndToast(...) · createInboxUndoStack(storageKey). Validates envelope before sending. Returns rejection on timeout (10s default).
4. reference/services/tailscale.mdDONETier 2 service contract per Connections Registry rule. Documents 4 tailnet machines, 3 users, source-IP allowlist pattern. Registry row added.
5. CLAUDE.md startup hookDONENew section "Dashboard Inbox check" added to Returning Mode. On session start, scan F:/Agentic-OS/inbox/, execute each pending file's _commands[0], archive to _processed/.
6. Memory + index pointerDONENew: feedback_dashboard_inbox_push_live.md. MEMORY.md updated with index entry.
7. Coolify reverse-proxy /inbox-api/*DONE9 Traefik labels added to Container Labels textarea on s8sk0ks4gk40kwsgco480cgg. Untick "Readonly labels" first; tick "Escape special characters" to protect bcrypt $ chars. Recipe banked at reference/services/coolify.md + feedback_coolify_custom_labels.md.
7.5. Windows Firewall rule (TCP 7392 from 100.64/10)DONESelf-elevating bat at tools/setup_inbox_firewall.bat. Scoped to Tailnet CGNAT range only — never publicly exposed.
8. Wire creative-review Push Live buttonPENDINGDrop-in: add <script src="../_shared/inbox-client.js"></script> to creative-review/index.html; add a "Push Live" button next to "Export Vault" that calls pushAndToast(envelope, { dashboard: 'creative-review' }). Same envelope as exportVault().
9. E2E test through CoolifyDONE 22:44 BSTFull loop closed. Browser-style request → Cloudflare → Coolify Traefik → private Tailnet → Windows Firewall → FastAPI → JSON written to disk. POST /inbox-api/jobs → HTTP 200 · client_ip=100.100.216.69 source=tailnet · file size 497B at F:/Agentic-OS/inbox/creative-review/. Test script: .tmp-drive-pull/e2e_test.py.

FAQ: Why does the request go through Cloudflare? (banked 22:50 BST)

Cloudflare here is DNS + CDN + WAF only — not a data store. The domain apps.nutritionalproducts.org resolves to a Cloudflare edge IP, so every request to that hostname hits Cloudflare first regardless of architecture. Cloudflare then proxies through to the Coolify VPS at 168.231.115.117.

No KV involvement. v5 deliberately rejected the Cloudflare Workers KV pattern (it was v1 in the rejected-iterations table above). Workers KV is only used by the separate quiz email relay Worker (white-flower-17a2-brainzyme-reviews), not by this inbox.

Cloudflare adds free TLS, edge caching for static dashboards, WAF, and DDoS protection at zero infra cost. The one rough edge we hit during testing: Cloudflare's default WAF flagged the Python urllib User-Agent (error 1010); a real browser doesn't trigger this. If you ever need to test programmatically, use a browser-like User-Agent header.

Coolify steps for Calum (when back)

Open http://168.231.115.117:8000/project/a0g0sscgcoo4cow44ckcok00/environment/vc8048wogo00c88gw4wk0g0k/application/s8sk0ks4gk40kwsgco480cggConfigurationAdvanced (or wherever Custom Labels live) and add the following Traefik labels:

traefik.http.routers.inbox-api-s8sk0ks4gk40kwsgco480cgg.rule=Host(`apps.nutritionalproducts.org`) && PathPrefix(`/inbox-api`)
traefik.http.routers.inbox-api-s8sk0ks4gk40kwsgco480cgg.entrypoints=https
traefik.http.routers.inbox-api-s8sk0ks4gk40kwsgco480cgg.tls=true
traefik.http.routers.inbox-api-s8sk0ks4gk40kwsgco480cgg.middlewares=inbox-strip,http-basic-auth-s8sk0ks4gk40kwsgco480cgg
traefik.http.routers.inbox-api-s8sk0ks4gk40kwsgco480cgg.service=inbox-backend
traefik.http.middlewares.inbox-strip.stripprefix.prefixes=/inbox-api
traefik.http.services.inbox-backend.loadbalancer.server.url=http://100.127.50.113:7392
traefik.http.services.inbox-backend.loadbalancer.passhostheader=true

Verify after deploy: curl -i -u admin:correct-horse-battery-staple-success https://apps.nutritionalproducts.org/inbox-api/health should return 200 with JSON containing "client_ip":"100.100.216.69" (the Coolify VPS Tailnet IP).

Once verified, run F:/Agentic-OS/tools/setup_inbox_server_task.bat on this PC to register the Task Scheduler entry, and the system is fully live.