Architecture (shipped 2026-04-22)
iPhone (Safari/Chrome on any network)
↓ HTTPS via Cloudflare Access email-gate
https://brief.josephbowens.com/
├── index.html (renders 4 approval channels)
└── /api/dismiss ← Cloudflare Pages Function (JS)
└── /api/dismissed ← Cloudflare Pages Function (JS)
↓ KV binding
Cloudflare KV namespace "skyrun_approvals" (id: f289e66a2fcd46af84beafb842a719d8)
↓ (next nightly-consolidation reads dismissed list, removes matching entries from local pending_*.jsonl)
What the user sees
Each approval item has TWO buttons:- Open → — deep link to native tool (Gmail, SmartLead, workbook) where the real approval/send happens
- Dismiss — marks item as handled; removes from queue instantly (optimistic UI, server-side in KV)
Files (on disk)
- Pages Function — POST dismiss:
~/Library/Application Support/SkyRun/pwa/functions/api/dismiss.js
{ id, channel }
- Writes KV key dismissed/{channel}/{id} with 30-day TTL
- Captures dismissed_by from Cloudflare Access authenticated email
- Pages Function — GET dismissed:
~/Library/Application Support/SkyRun/pwa/functions/api/dismissed.js
dismissed/* keys from KV
- PWA client-side JS calls on page load to hide already-dismissed items
- Client-side JS in index.html — appended by
build_pwa.py:
GET /api/dismissed → hide matching cards
- On Dismiss click: POST /api/dismiss → hide card + empty channel section if last item
KV namespace details
- Name:
skyrun_approvals - ID:
f289e66a2fcd46af84beafb842a719d8 - Binding to Pages project:
APPROVALS(available asenv.APPROVALSin Functions) - Account:
e38dfdc47924abcebc2227f3a2b8c9e2 - Key schema:
dismissed/{channel}/{id}→ JSON{id, channel, dismissed_at, dismissed_by} - TTL: 30 days (items auto-expire; nightly-consolidation should reconcile sooner)
Deployment
Everydeploy_pwa.sh run now deploys:
index.html+manifest.webmanifest(static)functions/api/*.js(Pages Functions — compiled by Cloudflare at deploy time)- KV binding already configured on the project via
PATCHAPI
Mac-side reconciliation — WIRED (Apr 22)
nightly-consolidation Section G reconciles KV ↔ JSONL every 11pm:
1. Lists all dismissed/* keys in KV via the CF API (bypasses Access using the server token in .env)
2. For each key, prunes matching entries from the relevant pending_*.jsonl file
3. Deletes the KV key after successful local prune (no-op if KV key was already dismissed more than once)
4. Appends audit record to dismissed_archive.jsonl (1-year retention)
KV ↔ JSONL stays in sync every 24h. Between reconciliations, the client-side JS hides dismissed cards via GET /api/dismissed, so the user sees a consistent state regardless of reconciliation timing.
Auth
All/api/* endpoints inherit Cloudflare Access — only Joseph.Bowens@SkyRun.com can POST to dismiss. Public (non-authenticated) requests get 302 to the CF Access login page.
Access app config (2026-04-24)
- App: "SkyRun BD Brief" — ID
26c5a66b-de66-4ac2-a63c-aa35951829de - Policy: "Joseph" — ID
3625ff64-6c39-4e1f-a5a4-01a3a6f257a2, decisionallow, ruleemail = joseph.bowens@skyrun.com - Policy session duration:
730h(~30 days) — raised from 24h on 2026-04-24 to reduce reauth friction on the PWA - App-level session_duration:
24h(fallback; irrelevant as long as only the Joseph policy exists, since policy-level overrides app-level) - Where to edit:
https://dash.cloudflare.com/{accountId}/one/access-controls/apps→ SkyRun BD Brief → Configure → Policies → Joseph → Configure → Session duration dropdown
Auth-expiry handling (PWA client — shipped 2026-04-24)
Fetch wrapper inbuild_pwa.py (apiPost) uses redirect: 'manual' + content-type check. When a POST hits an expired Access session:
- Detects
opaqueredirect/ non-JSON HTML response / CORS TypeError - Stores reason in sessionStorage
- Triggers
location.reload()— Access re-auth kicks in seamlessly (code email already cached in browser for 30d) - User taps Dismiss again → works
This was required because prior to the fix, POST got 302→cross-origin→CORS TypeError and the Dismiss button just showed "Retry" forever. KV showed zero dismissals ever written until 2026-04-24.
Rollback
See~/Library/Application Support/SkyRun/snapshots/2026-04-22_pre-live-ea/ROLLBACK.md for pre-v2 state + restore commands.
Future upgrades (NOT yet built)
/api/send-draft— execute Gmail API send directly; requires Gmail OAuth service token stored as CF secret/api/activate-campaign— for SmartLead auto-activate (big decision, probably better left to manual)- Push notifications on RED items via Cloudflare service workers
- Approval-queue-as-source (move from JSONL-primary to KV-primary — simplifies the data model)