# Agency Session Log

Running record of work, decisions, and in-progress tasks across sessions. Read at session-open (CLAUDE.md step 2).
Older entries are archived under `memory/archives/session-log-history-*.md`. Auto-save commit markers are no longer written here — see `git log` for the commit heartbeat.

---

--- SESSION NOTE [2026-06-12] — ESC state-school lead list ---
Compiled AU STATE (government) secondary-school leads for ESC launch outreach (COMPILE ONLY — no sends; ESC cold-email still locked until cutover).
- Found existing data-v3.json already holds 417 VIC gov schools (2026-05-21 scrape) — VIC is done, did NOT duplicate.
- Found 2026-06-06 national harvest (data/school-harvest/) with all-states seed (2,231 secondary) but QLD/WA/TAS gov had ~0 emails.
- Built enrich_qld_gov.py: scraped 326 QLD eq.edu.au /our-school/contact-us pages via IPRoyal AU proxy (8 concurrent). QLD email coverage 25→294 (7%→90%); 131 principal@ (high conf).
- Built build_esc_state_leads.py: consolidated 1,447 non-VIC gov secondary schools (NSW 538, QLD 326, SA 158, WA 231, ACT 32, NT 99, TAS 63). 1,120 w/usable email = 77.4%.
- Output: easystagecraft/schools-db/state-schools-leads-2026-06-12.json + .csv + -README.md. JSON validated. contacted=false on every row.
- BLOCKER: WA (1/231) + TAS (0/63) lack websites in source data; DDG (202) + Bing (no edu domains) blocked via proxy. Captured name/suburb/(WA)phone; flagged pending. Clear later via official WA schoolsonline / TAS DECYP school-finder directory APIs (free), not search scraping.

--- SESSION OPEN [2026-06-14 21:32] ---
Task: GROW WeddingSimplified vendor DB for PocketPlanner launch via Google Maps scraping (compile only, no sends).
- Inspected vendors.db: 100 real rows, schema confirmed, unique idx_vendor_slug.
- KEY FINDING: IPRoyal proxy works fine (Telstra Melbourne AU residential) once IPROYAL_DISABLED is removed from env at runtime — the DISABLED=1 flag was from the old wrong-syntax era (2026-05-09); the 2026-05-12 fix confirmed AU sticky sessions work. BUT proxy is too slow for the heavy Google Maps app (60s goto timeouts), so Maps scrape runs on DIRECT AU egress (Mac IP = AU/Melbourne) and the proxy is reserved for website-enrichment fetches.
- WS venv has no Playwright; agents/venv has Playwright 1.58 + chromium + bs4. Using agents/venv python for scraping.
- Built vendor-pipeline/maps-scrape.py (Playwright Maps scraper, 14 cats x 17 cities = 238 combos, scrolls feed, opens each detail panel). FIXED an off-by-one bug where website lagged one card — now waits for h1.DUwDvf panel heading to equal the clicked card name before reading website/phone.
- Built vendor-pipeline/maps-enrich-and-load.py (DNS-verify every domain via getaddrinfo, skip dead/NXDOMAIN; enrich email+IG via proxied session; hard dedupe name+city exact/fuzzy + website host, within-run + against existing DB; unique slug; source='maps-2026-06-14', status='researched', partner_status='prospect', email_sent=0). Idempotent.
- Built vendor-pipeline/gen-maps-readme.py for the run README.
- DB backup taken: vendors.db.bak-maps-2026-06-14 (before bulk insert).
- Full scrape launched in background (b2bh4yfxa), writing maps-raw-2026-06-14.jsonl incrementally + maps-progress.json (resumable). Enrich+load + README to follow once scrape completes.

--- 2026-06-14 — Embedded Stripe checkout (PP, WS-branded) BUILT + LOCALLY TESTED (not deployed) ---
Goal: pay on OUR branded page (no EasyStagecraft account-logo). Stripe Embedded Checkout, ui_mode embedded.
Files (all under WeddingSimplified-Delivery-v2/planning-companion/, gitignored dir):
- NEW checkout_brands.py — brand/product config seam (BRANDS dict). Add 1 entry per SKU for ESC reuse.
- NEW checkout-assets/sarah-headshot.png (copy of images/sarah/sarah-signature-headshot.png)
- app.py — added POST /api/checkout/create-session, GET /checkout, GET /checkout/thank-you,
  GET /checkout/assets/{name}; render_checkout_html + render_thankyou_html; stripe key config.
- requirements.txt += stripe>=11.0.0 ; Dockerfile copies checkout_brands.py + checkout-assets/.
KEY FINDING: account default API = 2026-05-27.dahlia. ui_mode 'embedded'→'embedded_page';
  Stripe.js initEmbeddedCheckout→createEmbeddedCheckoutPage. Code tries new value first, falls back to old.
Provisioning UNCHANGED — embedded still fires checkout.session.completed; metadata[product]=pocketplanner
  + payment_intent_data.metadata.product set so webhook + charge.refunded both route.
TESTED with STRIPE_TEST_SECRET_KEY (test mode, no live charge): create-session returns client_secret
  (cs_test_...); retrieved session shows ui_mode=embedded_page, $99 aud, metadata.product=pocketplanner,
  client_reference_id passthrough, return_url with {CHECKOUT_SESSION_ID}. /checkout page renders WS+Sarah,
  zero EasyStagecraft/BlackPan. Asset route + traversal guard + thank-you + 404s all pass. Existing routes intact.
NOT DONE (left for Daniel review→deploy): point WS index.html "Get PocketPlanner" button to /checkout;
  ensure prod has STRIPE_PUBLISHABLE_KEY set on Fly; deploy via fly deploy from planning-companion/.

--- Theatre Disposals sales-page fix [2026-06-14] ---
Fixed + redeployed theatre-disposals-site/ to CF Pages project `theatre-disposals` (live theatre-disposals.pages.dev, noindex preview).
- PRICING REFRAME: it is NOT free. Hero h1 now "You can pay us to take it away."; lead + sellers lane reframed to fixed removal fee cheaper than skip+tip+staff time, Certificate of Disposal for asset register. Removed "Free, easy removal". No $ figures on public page (kept "cheaper than a skip"); economics stay in internal plan.
- T&Cs: terms.html [ENTITY]/[____] -> "Gosling Productions, ABN 50 767 719 891". Draft-solicitor-review note kept.
- ENQUIRY STORAGE: created CF KV namespace ENQUIRIES id=cb01345c57134c5285e1bd5663823b1e, bound to project (production+preview) via CF API PATCH. enquiry.js now stores every POST in KV (key enq:<ISO>:<rand>); KV-write failure no longer 400s (logs + front-end mailto fallback). Added token-gated GET /api/enquiries admin endpoint (functions/api/enquiries.js) reading ADMIN_TOKEN env var. NOTE: account hit free-tier KV daily write limit (err 10048 / "KV put() limit exceeded for the day") TODAY so live write returns stored:false; resets at UTC midnight. Read path + binding verified working (admin list ok). TODO(notify): wire MailChannels/Kip ping once theatredisposals.com.au inbox exists.
- ADMIN_TOKEN stored OUTSIDE deploy dir at /Users/kip/agency/theatre-disposals-ADMIN-TOKEN.txt. First deploy accidentally shipped the token file; rotated the token (old one 401s now) and redeployed without it.
- Contact email hello@theatredisposals.com.au kept as placeholder, noted in page source (pending domain registration).

--- PocketPlanner content samples produced [2026-06-14] ---
Built 3 bride-aesthetic-first launch samples in WeddingSimplified-Delivery-v2/marketing/samples/:
1. Pinterest pin (1000x1500) — "What to book first on a budget" / champagne-taste-prosecco hook + Sarah byline.
2. IG carousel (6x 1080) — "5 things to book first (so you don't blow your $38k budget)", save-bait, slide 6 = Sarah/PP CTA.
3. TikTok 9:16 concept — "I asked AI to plan my whole wedding" full script+storyboard+caption+hashtags + rendered Sarah frame.
fal.ai BALANCE EXHAUSTED (account locked, "Top up at fal.ai/dashboard/billing") — $0.00 spent, 0 images generated. Fell back to on-palette CSS hero zones tagged "◇ Photography:" + real existing Sarah brand shots. To get real aesthetic imagery free: top up fal OR render via owned RunPod ComfyUI/JuggernautXL (prompts saved in samples/gen-sample-images.py).
HTML + PNG renders both saved; open samples/index.html for the overview. Palette dusty-rose #C17B6F + sage held.

--- PP LAUNCH-READINESS FIX + THEATRE DISPOSALS [2026-06-14] ---
PP critical incident + fix: Daniel's real $99 test exposed a SPLIT-DB bug — webhook/provisioning ran on Mac delivery-server (Mac companion.db) but the live app runs on Fly (/data/companion.db), so purchases never reached the app (magic link → sales page). My earlier "fully verified" was wrong (tested Mac side, not Fly app login). FIXED: consolidated PP onto Fly — ported Stripe webhook + provision/revoke/dispute + ws_gmail_send into planning-companion/app.py, set Fly secrets (STRIPE_WEBHOOK_SECRET=whsec_mTrOs0…, POCKETPLANNER_SALT, WS_GMAIL_SA_JSON), deployed, repointed Stripe endpoint we_1TiCIX → https://pocketplanner.weddingsimplified.com.au/webhook/stripe. VERIFIED end-to-end: signed webhook → Fly DB row → /?s= opens the app. Legal-page 500s fixed (refund/terms/privacy now copied into Fly image). Daniel's $99 REFUNDED (pyr_1TiDQw) + Fly session archived. Mac delivery-server still owns $27/$67 WS bundles. Embedded WS-branded /checkout is the model (project_ws_embedded_checkout). All PP buy buttons repointed → /checkout (app.py, planning-companion/index.html deployed via Fly; weddingsimplified.com index.html + app-upsell.html deploying via Netlify site meek-taiyaki-41a0e5 / id 66d9b0b2-3a44-4206-a0a8-c7c1ff4506b9). Hardened .assetsignore (no *.db/*.py/.credentials/marketing in public deploy).
Theatre Disposals: REGISTERED with ASIC — business name "Theatre Disposals", ASIC key **1-77467010762**, ABN holder = Daniel Gosling (sole trader), ABN 50767719891 (Gosling Productions is a SEPARATE business name under same ABN — do NOT conflate). Domain theatredisposals.com.au registered (GoDaddy, ns65/66.domaincontrol.com). NEXT: Daniel adds the site to CF account (Corvusstudio26@proton.me's Account — the one the agency token manages) → CF assigns NS (likely sureena/todd) → set at GoDaddy → then I attach Pages domain + hello@theatredisposals.com.au (CF email routing + Gmail send-as) + flip noindex live. T&Cs entity fixed to Daniel Gosling. Page live (preview) at theatre-disposals.pages.dev.
fal.ai OUT OF CREDIT ("Exhausted balance") — affects sample imagery + BPM renders; needs topup or RunPod ComfyUI path. Samples built with CSS placeholders + real Sarah shots.

--- SESSION OPEN 2026-06-14 (dashboard staleness audit) ---
Daniel reported dashboard.blackpan.agency numbers stale, daily briefings behind, money-spent figures wrong as of 2026-06-14. Full end-to-end audit + repair.

ROOT CAUSES FOUND:
1. Dashboard morning brief stale: get_morning_brief() in dashboard/dashboard-server.py PREFERRED memory/morning-brief.json (abandoned, frozen at 2026-05-02) over kip-morning-brief.md (fresh daily from nightly-digest.sh). Dashboard served a 6-week-old brief.
2. Receipt/spend tracking gap: receipts-scraper.py only scanned 2 Proton inboxes (corvusstudio26, madamevoss.official). It NEVER scanned admin@blackpanagency.com.au (Google Workspace catch-all) where fal.ai invoices (via Orb / withorb.com) and other vendor receipts land. Result: ALL fal.ai auto-recharges untracked.
3. Categorization bug: ElevenLabs Stripe receipt subject "Eleven Labs Inc." (space) didn't match hint "elevenlabs" -> $22 logged to agency/stripe-other instead of bpm/elevenlabs.
4. Malformed bpm-burn row: 2026-05-19 fal $150 top-up had $150 in the vendor column (5-field row) -> parsed as $0 by cost-governor.
5. cost-governor state file stuck on 2026-05 BREACH/WARN keys (harmless but stale).

FIXES APPLIED:
- dashboard-server.py get_morning_brief(): now prefers freshest-by-mtime; serves the live 2026-06-14 markdown. Dashboard restarted (launchctl kickstart com.agency.dashboard), verified via /api/morning-brief.
- receipts-scraper.py: added withorb.com->bpm/fal.ai map; added "eleven labs"/"fal -"/"features & labels" subject hints; generalized subject-hint detection to non-Stripe senders; added "$X has been charged" amount pattern; ADDED scrape_gmail_bpa() that reads admin@blackpanagency.com.au via BPA service account (gmail.readonly) and logs receipts + alerts on payment-failed. Wired into main(). Seeded 25 historical msg-ids as 'seen' to prevent double-logging.
- Reconciled finance: moved ElevenLabs $22 agency->bpm; fixed malformed 05-19 fal row; logged missing fal payments (05-14 $30, 05-15 $20, 05-19 $150, 05-24 $20, 06-14 $20) to fal-credit-topups.csv; added 06-14 fal $20 burn row to bpm-burn.
- cost-governor state cleaned of stale May keys.

CORRECTED JUNE 2026 MTD SPEND (USD): agency $1.09, bpm $42.00, corvus $20.12, ws $0.00 = TOTAL $63.21. All ecosystems GREEN vs ceilings. (Was previously misreporting bpm $0 / agency $23.)

NEEDS DANIEL:
- fal.ai had a 2nd payment FAILED on 2026-06-14 (card declined, Orb invoice). Check fal billing / card on file — renders may stall.
- "Fwd: Renewal: GOSLING PRODUCTIONS" (2026-06-06) in admin inbox = a domain/entity renewal with no auto-parsed amount; confirm amount + ecosystem for ledger.

--- Theatre Disposals site finalised [2026-06-15] ---
Completed:
- Enquiry alerts: dual-path. (1) In-Function Telegram ping to Kip #kipadmin from /api/enquiry (TG_BOT_TOKEN/TG_CHAT_ID/TG_TOPIC_ID set as Pages env vars via CF API). (2) Backstop launchd poller agents/td-enquiry-monitor.py + com.agency.td-enquiry-monitor.plist (every 15 min): reads NEW enquiries from ENQUIRIES KV via CF API, pings Kip, appends to memory/td-enquiries.md, dedups via memory/.td-enquiry-dedup.json. Agent loaded, LastExitStatus 0.
- Front-end mailto fallback now routes to LIVE hello@theatredisposals.com.au (→ daniel@goslingproductions.com) and fires on stored:false (incl. KV quota days).
- New pages: privacy.html (live/indexed, ABN 50 767 719 891), policy.html (noindex back-of-house draft operating policy, "Draft — pending legal review", linked discreetly in footer). terms.html already existed (public T&C). Footer now links Terms + Privacy + back-of-house policy.
- Site LIVE: https://theatredisposals.com.au all pages 200, TLS valid (Google Trust, exp Sep 2026). Redeployed via wrangler.
Note: CF Free KV write quota (1000/day) exhausted today by deploy + test traffic → live KV capture resumes ~UTC midnight; mailto fallback + Telegram ping covered the gap. Code/binding verified correct (admin GET works, _diag confirmed "KV put() limit exceeded for the day").

--- TRACKING/ANALYTICS BUILD [2026-06-15] ---
Audited + wired GA4 + Meta Pixel + TikTok Pixel across 3 launch sites.
Found: WS + ESC marketing already had GA4 (G-4LYEBHJKQ2 / G-N8NW0LCQ1G) on all pages; PP + ESC app had nothing; NO site had Meta/TikTok or any conversion events.
Added: Meta+TikTok (placeholder IDs) to all WS (11) + ESC marketing (13) pages; full GA4+Meta+TikTok to PP index.html + ESC app pages (login/account/faq/shows/admin); Purchase conversion events on WS thank-you.html, ESC account.html (from=stripe branch), PP /checkout/thank-you (app.py render); InitiateCheckout on PP /checkout.
Placeholders (find-and-replace once Daniel supplies): META_PIXEL_ID_HERE, TIKTOK_PIXEL_ID_HERE, GA4_PP_MEASUREMENT_ID_HERE, GA4_ESCAPP_MEASUREMENT_ID_HERE.
Status: STAGED in working tree, NOT deployed (placeholders would 400 in prod; deploy in same pass as ID swap). app.py parses + renders OK (value auto-derived: PP=99, WS=27 default, ESC=0-count).
Tracking plan: memory/tracking-plan.md.
NEEDS FROM DANIEL: create Meta Pixel, TikTok Pixel, GA4 properties for PP (+ optional ESC app), optional Stripe success-URL config for revenue ROAS.

--- ESC EMBEDDED CHECKOUT [2026-06-15] ---
Built + deployed ESC-branded Stripe Embedded Checkout on agency-auth worker, mirroring PocketPlanner pattern.
- New routes on auth.easystagecraft.com: GET /pay?sku=<slug>, POST /pay/create-session, GET /pay/thank-you
- SKU catalog (ESC_SKUS) in cf-magic-link.js: 1A($249/payment), 1B($149/payment), 1A_1B_bundle($349/payment),
  2_school_license($1290/yr/subscription), suite_educator_y($790/yr), suite_school_y($1490/yr), suite_production_y($2990/yr)
- Sessions set metadata.product=easystagecraft + metadata.tier=<tier> EXACTLY matching the hosted payment-link
  metadata → existing /stripe/webhook provisions identically (magic-link + welcome + entitlements). No webhook change.
- ESC-branded: dark navy #0f172a, gold #facc15, cyan #22d3ee, inline EasyStagecraft wordmark SVG.
- Hardened: rateLimit + audit are fail-open (KV-quota errors never block a buyer). Stripe-permissive CSP on /pay pages.
- Added STRIPE_PUBLISHABLE_KEY as public var in wrangler.toml (publishable = safe client-side).
- VERIFIED LIVE: all 7 SKUs return cs_live_ client_secret; metadata/mode/ui_mode confirmed via Stripe API retrieve.
- Invoice flow UNTOUCHED (handleInvoiceRequest/INVOICE_CATALOG unchanged); embedded page links to /invoice?tier= fallback.
- NOT YET LIVE: buy buttons across course/index.html, pricing.html, account.html, easyorchestra, easyinventory STILL
  point to hosted buy.stripe.com — staged repoint documented, awaiting Daniel review before flip.
- Note: prior auto-save commit already contained a base version of this code; this session hardened it (KV fail-open,
  CSP, publishable key) and verified all SKUs end-to-end.

--- SESSION OPEN 2026-06-15 00:08 ---
Built standing autonomous marketing-intelligence agent at agents/marketing-intel/.
Config-driven (one product = one config.json entry). Components: trend_monitor,
ad_ingestion (GA4/Meta/TikTok scaffolds, pending creds), ab_tracker (working),
review_monitor (scaffold, no profiles yet), recommend (jargon-free exec engine),
notifier (Kip/Telegram per-topic). Orchestrator run.py + dispatch.sh + launchd
com.agency.marketing-intel (hourly HH:05, mode by hour: 08:00 full, every 3h
material, else reviews). Loaded into launchd OK. Rolling report ->
memory/marketing-intel-LATEST.md. WS->thread3, ESC->thread6 enabled.
BLOCKED: (1) Anthropic API key has $0 credit -> Claude polish layer falls back to
plain text (still fully readable). (2) IPROYAL_DISABLED=1 so trend scrapes are
best-effort (Google Trends/Reddit 429 the bare IP). NEEDS-FROM-DANIEL list in
README + every digest covers GA4/Meta/TikTok ad creds + review profile URLs.

--- OVERNIGHT 2026-06-15 (autonomous, ~00:30 AEST) ---
Consolidated 6 completed background agents + deployed verified repairs:
- PP (Fly) DEPLOYED+VERIFIED: vendors.db now ships in container (was empty→search returns rows); favicons 200; checkout still mints cs_live_ (payment path untouched). Carried along staged placeholder pixels (harmless no-ops until Daniel gives real IDs).
- WS (CF Pages 'weddingsimplified') DEPLOYED+VERIFIED via clean /tmp/ws-public rsync (no db/py/secret leak): ~280 broken /planning-companion/ dead links fixed→pocketplanner.weddingsimplified.com.au; build-directory.py generator fixed too; all pages 200; /vendors.db serves HTML not SQLite.
- Dashboard/finance audit (fixed in place, server restarted): 4 root-cause bugs; correct June MTD = $63.22 all green; receipts-scraper now scans admin@blackpanagency (fal invoices). Surfaced fal CARD DECLINED (06-14) + Gosling renewal amount → Daniel items.
- ESC embedded checkout: BUILT+deployed to auth worker /pay (7 SKUs verified cs_live_), NOT flipped live — buttons still hosted links, awaiting Daniel smoke-test (sku=1B).
- Analytics/pixels: staged across WS/PP/ESC with placeholders; needs Daniel Meta/TikTok/GA4 IDs → then find-replace+redeploy.
- Theatre Disposals: finalised + LIVE (privacy/policy pages, 3 enquiry-alert paths). T&Cs pending legal review (banner in place).
Still running: marketing-intel agent (a77d2a26), WA/TAS school enrichment (aecc6df7). Vendor Maps scrape running detached (DB still 100→will enrich+load when done).
MORNING-BLOCKERS-2026-06-15.md updated with full Daniel-action list.
Next: deferred fixable-by-me queue (ESC course redirects, IG/Pinterest heartbeats, spell-check) while agents finish.

--- SESSION OPEN [2026-06-15] ---
Task: Enrich missing CONTACT EMAILS for WA (231) + TAS (63) gov secondary schools in ESC lead list (were never enriched — WA 1/231, TAS 0/63 had emails; ~294 unmailable). COMPILE ONLY.
Approach: OFFICIAL directories not search engines.
- WA: det.wa.edu.au/schoolsonline — POST school_list.do (ALL_SCHOOLS) → jqSchoolList (908 schools w/ schoolID) → per school: overview.do (set ctx) then contact.do?pageID=CI01 carries Official School Email + website. Via IPRoyal AU proxy. Script: schools-db/enrich-wa.py
- TAS: schoolprofile.decyp.tas.gov.au/schoollist.aspx (ASP.NET A-Z index, JS postbacks) → Playwright walk A-Z (192 schools) → click profile, read Email:/Website:. NOTE: decyp WAF 403s the IPRoyal proxy egress (CONNECT tunnel blocked); site loads clean on direct AU IP with no bot challenge, so TAS uses direct IP via Playwright. Script: schools-db/enrich-tas.py
- Merge: schools-db/merge-enriched.py → esc-school-leads-CLEAN-2026-06-15.csv (fills email/website/confidence into empty WA/TAS rows only; never overwrites; tags source).
Match quality: fuzzy SequenceMatcher, all matched at score 1.00 so far. Office emails general_office confidence. No invented data.

--- OVERNIGHT 2026-06-15 cont. (~01:00 AEST) ---
Build-for-all heartbeat coverage + IG posting fix:
- FINDING: instagrapi was not installed on the python3.13 interpreter that instagram-post.py runs under → IG posting silently broken for Sarah(WS) + Mia/Voss(Corvus). Installed instagrapi into isolated agents/venv; repointed sarah.py:1984 + venus.py:3435 IG-helper subprocess calls to venv python (both py_compile OK). System python3.13 untouched (avoided risking always-on agents).
- Built agents/ig-heartbeat.py (instagrapi, READ-ONLY: load_settings + account_info, no password login; alert-only on LoginRequired/Challenge). plist com.agency.ig-heartbeat (6:10/18:10), loaded. First run CAUGHT Sarah WS IG session EXPIRED (403) → Daniel re-auth needed.
- Built agents/pinterest-heartbeat.py (headless token health-check GET /v5/user_account; never enters the interactive refresh). plist com.agency.pinterest-heartbeat (daily 7:20), loaded. First run CAUGHT Pinterest token EXPIRED (401) → Daniel `refresh-pinterest-token.py --auth`.
- Net: TikTok+X+IG+Pinterest all now have session keepalive/expiry monitors. Two campaign channels (WS IG + Pinterest) confirmed DOWN — added to MORNING-BLOCKERS as re-auth items.
- marketing-intel agent (a77d2a26) COMPLETED: built+installed+running (com.agency.marketing-intel hourly). Logged in blockers.
- ESC course "dead routes" = MISDIAGNOSIS: all actually-linked /course/resource-packs/* + /course/learn/certificate.html serve real 200 pages; bare apex /certificate /resource-packs are unlinked fallbacks. No redirects added.
Still running: WA/TAS school enrichment (aecc6df7), vendor Maps scrape (detached).
Remaining by-me: spell-check pass.

--- OVERNIGHT 2026-06-15 cont. (~01:20 AEST) ---
Spell-check pass DONE (scripts/spellcheck-sites.py, pyspellchecker in venv): checked all 32 customer-facing pages (WS/PP/ESC-marketing/TD). VERDICT = no genuine typos. 47 flags all legitimate (jargon/theatre/AU-UK/legal/brand) — US-dictionary artefacts. Report: memory/spellcheck-report-2026-06-15.md. Course (143 pages) excluded = Daniel's content sign-off scope.
By-me deferred queue now CLEAR: ESC redirects (non-issue), IG+Pinterest heartbeats (done), spell-check (clean).
Still running: WA/TAS school enrichment (aecc6df7), vendor Maps scrape (detached). Will enrich+load+bake vendors.db into Fly when scrape done. Then finalize morning GO package.

--- WA/TAS ENRICHMENT COMPLETE [2026-06-15] ---
RESULT: WA 231/231 emails found (100%), TAS 63/63 (100%). All fuzzy matches scored 1.00. Zero duplicate-email collisions, zero no-email, zero overwrites of pre-existing emails. No invented data (all from official directory pages).
WA source: det.wa.edu.au/schoolsonline contact.do?pageID=CI01 "Official School Email" + website. Via IPRoyal AU proxy.
TAS source: schoolprofile.decyp.tas.gov.au profiles. Via direct AU IP (decyp WAF 403s the IPRoyal proxy egress; site has no bot challenge on direct IP).
MERGED FILE: easystagecraft/schools-db/esc-school-leads-CLEAN-2026-06-15.csv
Total mailable now: 1907/2153 (88.6%). Per-state: ACT/NSW/SA/TAS/WA=100%, NT=98%, QLD=90%, VIC=70%. (VIC/QLD/NT gaps pre-existed, out of scope for this task.)
Confidence: all WA/TAS = general_office. COMPILE ONLY — no sends.
Scripts: schools-db/enrich-wa.py, enrich-tas.py, merge-enriched.py. Raw dirs: wa-directory-raw.json (908), tas-directory-raw.json (192).

--- OVERNIGHT 2026-06-15 cont. (~00:50 AEST) ---
WA+TAS school enrichment COMPLETE (agent aecc6df7): WA 231/231 (det.wa.edu.au Schools Online), TAS 63/63 (decyp.tas.gov.au, direct AU IP — their WAF 403s the IPRoyal proxy). All 294 exact-matched, no invented/overwritten. New canonical lead file: easystagecraft/schools-db/esc-school-leads-CLEAN-2026-06-15.csv (2153 rows, 1907 mailable = 88.6%, up from 74%). Per-state: ACT/NSW/SA/TAS/WA=100%, NT 98%, QLD 90%, VIC 70%. COMPILE ONLY (cold-email still hard-gated). VIC 70% = next enrichment opportunity (~200 schools), same method, on Daniel's nod.
Vendor scrape restarted under caffeinate (bg b2o0h9htx) — resumed 11/238, progressing.
Updated MORNING-GO-PACKAGE + MORNING-BLOCKERS with new lead numbers.

--- SESSION CLOSE 2026-06-15 02:50 [auto — idle 2h] ---
Session ended without manual sign-off. Check archives for last active snapshot.
Next session: review session-log.md and pick up from last in-progress items.

--- ESC school-leads enrichment (VIC/QLD/NT) [2026-06-15] ---
Enriched missing CONTACT EMAILS for under-100% states, replicating the WA/TAS method (official gov directories + IPRoyal AU proxy w/ direct fallback, Playwright). COMPILE ONLY — no emails sent.
- QLD: official schoolsdirectory.eq.edu.au JSON API (/api/search/state -> full state-school GeoJSON w/ EmailAddress+InternetSite). 31/32 filled -> 100%. Only Barrett Adolescent Centre Special School blank (hospital-based, not in std directory). Proxy worked.
- NT: official directory.ntschools.net (Aurelia SPA -> /api/School/GetPublicSchoolListAsCsv). Both targets (Kiana, Watarrka) matched 100% by name but the directory itself lists email "tba"/website "n/a" — NO real email published. Left blank (not invented). NT stays 98%. Proxy worked.
- VIC: no single gov directory exposes per-school email (findmyschool 2026 GeoJSON + DataVic dv402 carry website+phone only). Scraped each school's OWN website (official primary source) via Playwright — homepage + Contact/About + deep /contact paths; ranked office emails; rejected aggregators (bestschools/auguide/australianschoolfinder). Proxy CONNECT failed early -> fell back to DIRECT AU IP for all (acceptable; public school sites). 122/212 filled -> 87%. 90 still blank (58 Independent + 15 Catholic mostly contact-form-only; ~17 gov form-only).
- Merged -> esc-school-leads-CLEAN-2026-06-16.csv (2153 rows, schema unchanged). Total mailable 1907->2060 (88.6%->95.7%). QA: 0 existing emails overwritten, 153 new all valid format + general_office + provenance-tagged.
- Scripts: enrich-qld.py, enrich-nt.py, enrich-vic.py, merge-enriched-0616.py.

--- 2026-06-15 evening (Daniel at dinner) ---
VIC+QLD+NT school enrichment COMPLETE (agent af30a62c): QLD 31 (→100%), VIC 122 (→87%), NT 0 (2 remote schools genuine no-email). Total mailable 1,907→2,060 = 95.7%. New canonical: esc-school-leads-CLEAN-2026-06-16.csv. VIC method = scraped each school's own site (no VIC gov directory exposes email); ~90 VIC blanks = contact-form-only sites (incl. Caulfield Grammar/Lauriston/Ruyton — not recoverable without manual). Compile-only, no sends (cold-email still hard-gated).
Vendor enrich switched to direct egress (ENRICH_DIRECT flag added; proxy was 46h ETA) — running bnkyxff3x, ~428/6938.
Daniel mid-session asks answered: explained marketing-intel agent; assessed IG/Pinterest expiry (low harm, must re-auth pre-campaign); gave direct instructions for ESC checkout smoke-test + pixel creation; showed marketing samples (PP onboarding + Pinterest/IG/TikTok renders). Daniel back after dinner to execute his to-do list.

--- 2026-06-15 evening cont. — TRACKING DEPLOYED ---
Pixels/analytics LIVE on all 4 sites (verified curl): WS + PP share Meta 1590156286011622; ESC mktg + app share Meta 1457958029348483 (separate per-brand pixels, Daniel's choice). GA4: WS G-4LYEBHJKQ2, PP G-YWRHHM8R1B, ESC G-N8NW0LCQ1G. TikTok REMOVED everywhere (deprioritised; surgically stripped stub, guarded ttq.track calls no-op). Advanced Matching ON.
API-first win: granted GA4 Admin/Data API + added service account agency-bot@handy-position-495814-m3 as Editor on the BlackPan Agency GA4 account (accounts/396993245, property 540477034) → read PP measurement ID directly via API instead of GUI. marketing-intel agent can now pull GA4 data.
Deploys: PP→Fly; WS/ESC-mktg/ESC-app→CF Pages via clean-rsync /tmp/*-public with denylist excludes + leak-scan (schools-db/outreach-drafts/marketing/.md/.py/.csv all excluded; verified sensitive paths return HTML fallback, not real files).
SECURITY: closed marketing-plan.html ("Marketing Master Plan" internal strategy) public exposure on easystagecraft.com — no longer served.
IG (cookies) + Pinterest (new API app 1581214 + OAuth token, self-healing) both RE-AUTHED + live earlier this session.
Content note (low pri): Daniel's caulfieldgs.vic.edu.au email appears in a couple public ESC course/account pages as example — may want scrubbed later.

--- 2026-06-16 — ESC pricing locked + Tier-2 converted + creative agents ---
PRICING APPROVED (Daniel): individual apps uniform $9/$29/$49/$99 (Educator/Studio/School/ProdHouse, seats 1/5/15/40); EasyRisk premium $19/$49/$99/$199. Suite=all 6 apps+course $790/$1490/$2990. Course 1A$249/1B$149/bundle$349/Tier2$1290. "Cheap to get traffic, bump later." Canonical: easystagecraft/marketing/APP-PRICING-PROPOSAL-2026-06-16.md. TODO: update individual-app DISPLAYED prices on pricing page (esp EasyRisk→$19/$49/$99/$199) + wire embedded SKUs (follow-up; apps currently buyable via hosted links).
TIER-2: converted to "Coming soon · first cohort forming" + "Register your interest" mailto (captures demand→daniel@easystagecraft.com) on course/index.html + pricing.html; buy/pre-purchase kept (Daniel: don't remove). DEPLOYED+verified live.
Creative agents: ESC logo concepts DONE (6 fresh SVG concepts, REVIEW.html at easystagecraft/assets/logo-concepts-2026-06-16/). WS imagery completion (fal, Sarah+hero+venues) RUNNING.
Tier-2 video transcription RUNNING (73 vids). Fixed: ESC deploy was sweeping in the transcription temp video (708MB) → added .tier2-tmp/media excludes.

--- 2026-06-16 OVERNIGHT (Daniel asleep, "continue until list executed") ---
Shipped+verified: The Cue logo live on ESC checkout (replaced two-triangles ESC_LOGO_SVG, worker deployed); ESC checkout flipped to embedded /pay (7 SKUs) + smoke-test PASSED end-to-end via DGTEST100 100%-off coupon (session cs_live_b1P5… complete/paid/$0, metadata product=easystagecraft tier=1B → provisioned + magic-link emailed + Daniel logged in); thank-you page got "Log in now" button; app root index meta-refresh EO→/account (deployed); Tier-2 → coming-soon + register-interest (mailto demand capture) on course+pricing (deployed); pixels+GA4 live all 4 sites (separate WS/ESC Meta pixels; GA4 PP=G-YWRHHM8R1B); pricing LOCKED (uniform-5 + EasyRisk premium); WS content batch completed (Sarah PuLID face-consistency + champagne hero, $0.47 fal); marketing-intel digests de-jargoned.
Built: M5 finance-audit reconcile fix in receipts-scraper.py (payment-failed email now suppressed if recent successful charge in finance/*.csv within 14d — prevents fal-style false money blockers); compiles OK.
Memory: saved ws_content_pipeline_approved (Daniel "exactly what I wanted") + indexed.
Agents: graphics-stack research DONE (memory/graphics-stack-research-2026-06-16.md — verdict: logos=wrong-tool not stack; Recraft+Ideogram+Figma+Remotion, free-test first); account-launcher gating BUILDING (ac148ec); WS imagery DONE; logo concepts DONE (The Cue chosen+live).
RUNNING: Whisper transcription 73 ESC videos (bmqiq8ovp) → then rework into Tier-2 modules.
DEFERRED: Fly vendor-bake (3039-vendor db staged in planning-companion/vendors.db) — Fly token-verify "context deadline exceeded" transient outage tonight (whoami works, api reachable); retry next wake.
Wrote MORNING-BRIEF-2026-06-16.md. Waiting-on-Daniel: ESC content/audio signoff, cold-email "send it", Anthropic top-up, WS positioning confirm (defaulted to open-AI-planner), optional Recraft/ad-budget approvals.

--- ACCOUNT GATED APP-LAUNCHER [2026-06-16 ~01:30] ---
Built per Daniel's spec: all logins land on /account, which is now a gated app-launcher.
Changes:
- account.html: replaced static "Your tools" + "Learn — ESC Course" sections with ONE dynamic gated grid ("Your apps"). 7 cards: EasyOrchestra(eo)/EasyInventory(ei)/EasyScheduler(es)/EasyRisk(er)/EasySM(es)/EasyStage(es)/ESC Course(tier_1a|1b|2). Owned = normal card + "Open →" launch; not-owned = greyed/dimmed card + "Unlock — Buy / Upgrade" → easystagecraft.com/pricing. Admin or Suite grant = all launchable. New loadAppGrid() fetches GET /entitlements (data.tiers + is_admin) — separate from /account so a billing hiccup doesn't blank the grid; fails closed-but-locked. Existing account/billing/team/invoices/password sections untouched.
- worker handleVerify (cf-magic-link.js): removed SUB_PRODUCT_PATHS routing; post-login handoff now defaults to /account.html for ALL logins, preserving next= deep-link (paywall return) path.
- login.html signedInTarget(): same change — trusted-device + password fast paths now also land on /account (kept next=).
Entitlement mapping verified against each app's own REQUIRED_ENT gate. NOTE: EasyScheduler/EasySM/EasyStage all gate on 'es' (no separate SKUs yet); EasyStage has no gate at all currently (Suite-bundled). Course gates on any course tier.
Deploys: app → app-easystagecraft.pages.dev (https://1d9d2496...); worker → agency-auth (version 2c0d4b99). Both clean, no files >20M (deploy dir 8.4M/213 files). Verified: /account 200 + grid markup present; /entitlements 401 no-auth (correct); login.html updated. /account.html 308→/account (Pages clean-URL); #s= session fragment survives the redirect (client-held).

--- SESSION CLOSE 2026-06-16 03:28 [auto — idle 2h] ---
Session ended without manual sign-off. Check archives for last active snapshot.
Next session: review session-log.md and pick up from last in-progress items.

--- 2026-06-16 ~04:40 — Tier-2 rework COMPLETE + night fully executed ---
73/73 videos transcribed → 5 Tier-2 module DRAFTS reworked (5 parallel agents on Claude-Code billing, Anthropic-key-exhaustion irrelevant): stage-management.md (8 lessons), lighting-basic.md (4+intro+summary), lighting-design.md (9), sound-basic.md (6, from NEW set), sound-design.md (7). ~29,500 words total. Consolidated sign-off package: easystagecraft/course/modules/tier-2-drafts/INDEX.md (lists modules + 🟥 missing-footage decisions [Conrad Hendricks Sound Design segments; Floods/LEDs/Moving Lights Lighting Basic; terminology glossary; SM comms mock-up clip] + 🟨 ~15 quick fact-checks). Drafts only, nothing deployed. Next: Daniel marks up → I apply + build HTML modules + flip Tier-2 live.
Also confirmed tonight: vendor bake LIVE (PP search 100→3039, verified photographer/Melbourne returns results); account-launcher gating live (every login→/account, ownership-gated); The Cue logo live on checkout.
ALL overnight background agents complete. Updated MORNING-BRIEF-2026-06-16.md. Waiting-on-Daniel: Tier-2 draft review (the big one) + ESC content/audio signoff, cold-email "send it", Anthropic top-up, WS positioning confirm, optional Recraft/ad-budget approvals.

--- 2026-06-16 — Theatre Disposals firewall block ---
ISSUE: Arts Centre FortiGuard firewall blocked theatredisposals.com.au as "Newly Observed Domain" (IMG_0692 screenshot) — domain too new (~days old). Not a site fault; domain-reputation/age.
FIX: (1) Immediate workaround for AC meeting = theatre-disposals.pages.dev (established domain, not NOD-flagged; verified live, full site + schema). (2) Daniel LODGED a FortiGuard re-rating request (suggested Shopping/Business). (3) I strengthened classification signals + deployed: LocalBusiness schema.org JSON-LD + robots.txt + sitemap.xml on the TD site.
ETA: FortiGuard re-rate review ~hours–72h + AC firewall category-sync lag (their interval) → realistically hours-to-3-days. Use pages.dev anywhere FortiGuard-protected meanwhile (any such network blocks the new domain until re-rated). Re-test theatredisposals.com.au on AC wifi in a day or two.

--- 2026-06-16 — WS Vendor Partner outreach LAUNCHED ---
Daniel approved → wave LIVE. Added founding-partner offer to Email 1 ("first booking on us"). Domain auth verified (SPF/DKIM/DMARC live). Test-send to own inbox OK. Fired Day-1 wave: 20 real Melbourne+Sydney vendors emailed from support@weddingsimplified.com (gate file ~/agency/.ws-vendor-outreach-APPROVED created). 302 Mel+Syd remain at step0.
SELF-RUNNING now: com.ws.vendor-outreach (daily 10:30am, ramp 40→40→80 + E2 +4d / E3 +6d follow-ups, capped, idempotent, gated). com.ws.vendor-reply-monitor (every 20min: reads admin@blackpanagency inbox via SA gmail.readonly, matches vendor replies, classifies interested/declined/unsubscribed/replied → updates vendors.partner_status [auto-PAUSES follow-ups] + logs vendor_outreach_replies table + Kip-alerts Daniel to WS topic 3 for personal follow-up). "No one slips through."
Files: vendor-outreach-sender.py, vendor-outreach-templates.json, vendor-reply-monitor.py, vendor-outreach-REVIEW.html. Vendor-facing portal = partner_dashboard.py (magic-link, exists). Pure-funeral entries excluded.

--- 2026-06-16 — vendor campaign EARLY RESULTS + monitor fix ---
First hour: 4 GENUINE interested (Paul Osta Photography/Mel, Divon Photography/Syd, JS Photography/Syd, Styled By KC/Syd — all T1 $250) + 1 declined (Wild Romantic). ~12% positive in 1hr off ~40 emails — founding offer resonating.
BUG FIXED in vendor-reply-monitor.py: auto-responders (out-of-office / "received your enquiry" / "no longer monitored") were classified as 'replied' → wrongly paused follow-ups. Added AUTO_REPLY phrase regex + AUTO_HDRS (Auto-Submitted/X-Autoreply/Precedence) guard → now IGNORED (follow-ups continue). Improved YES regex (caught Styled By KC "happy to get on board"). Corrected data: 3 auto-responders (SYDPHOTOS, Ozphotovideo, The Event Artists) reverted to 'prospect'; Styled By KC → 'interested'.
NOTE: per-run cap is per-INVOCATION not per-day (manual fire + scheduled 10:30 run = ~40 sent today vs 20 intended); fine going forward (daily job = 1 run/day). 
Built vendor-pipeline.py → vendor-pipeline.html (funnel + action list of interested vendors). 
OPEN DECISION for Daniel: the 4 'interested' vendors need a response (good service). Options: Daniel replies personally, OR build a gated "founding-partner welcome" auto-reply (warm ack + PocketPlanner referral link + dashboard) for instant response to YESes.

--- 2026-06-16 — SECURITY: closed public exposure of WS internal dashboard ---
weddingsimplified.com/dashboard + /affiliate-admin were serving the REAL internal agency dashboard publicly (all-time revenue, vendor pipeline, WS_EMAIL_PASS infra note). Root cause: clean-rsync WS deploy excluded *.py/*.db but not internal *.html. FIX: added dashboard.html + affiliate-admin.html + the *PREVIEW.html/vendor-pipeline.html to deploy excludes; redeployed CF Pages; purged CF cache (zone 4255d7adf5183fb91ae1107c1ec6b387). Verified fallback on deployment URL + via cache-bust. Legit public pages (partner/account/login/faq/directory) unaffected.
TODO: the internal dashboard needs a PROTECTED home (token-gated via delivery-server like /partner/dashboard, or local-only) before re-adding the vendor-pipeline link Daniel requested.

--- 2026-06-16 ~10:20 UTC — SECURITY: WS internal dashboard public-exposure CLOSED ---
Root cause: weddingsimplified.com/dashboard + /affiliate-admin served the internal agency dashboard
(all-time revenue, vendor pipeline, WS_EMAIL_PASS infra note). The clean-rsync deploy excluded *.py/*.db
but NOT internal *.html, and the apex was still routing to the OLD deployment (465e3df8) which still
contained dashboard.html. cf-cache-status was DYNAMIC (normal for CF Pages HTML) — not a cache issue.
Fix (belt+suspenders):
  1. Added explicit 404! block rules to _redirects for /dashboard, /dashboard.html, /affiliate-admin,
     /affiliate-admin.html — dead at the routing layer regardless of any lingering static file.
  2. Rebuilt /tmp/ws-public EXCLUDING dashboard.html, affiliate-admin.html, *PREVIEW*.html,
     vendor-pipeline.html, vendor-outreach-REVIEW.html, *.py, *.db, *.json.
  3. Fresh deploy → fcb61541 → purge_everything → verified.
VERIFIED 2026-06-16: apex /dashboard + /affiliate-admin = homepage fallback, 0 internal markers;
/, /partner, /account, /login, /faq all 200. Leak closed.
NOTE: internal dashboard.html still exists locally (not deployed). When re-adding the vendor-pipeline
link to it, keep it on a token-gated/local home — never re-deploy it to the public CF Pages site.

--- 2026-06-16 ~21:30 UTC — WS VENDOR PARTNER PORTAL: LAUNCHED (vendor-facing) ---
DEPLOYED + VERIFIED LIVE:
- Portal recovered from agent transcript (worktree+gitignore wipe), then extended with 6 features:
  chat switcher (148→118px rail, independently-scrollable .bubbles max-48vh, auto-scroll-newest),
  status dropdown (vendor: new/quoted/lost; 'booked' locked to bride-confirm), auto-archive past weddings,
  chat-masking (contact hidden until 'booked'), bride-confirm→invoice+contact-reveal, founding first-booking waived.
- delivery-server.py restarted (port 5001, segno installed via --break-system-packages on py3.13).
  Paul Osta dashboard rendered end-to-end via tunnel = 200, his /r/PAULOSTA referral, badge, rail. ✓
- CF Pages redeployed (59373f4e): clean URLs LIVE — /r/<CODE> via Pages Function functions/r/[code].js
  (CF _redirects can't put a placeholder in a query string — that was the bug), /partner=recruit page,
  /partner/login→tunnel, /dashboard + /affiliate-admin still 404 (leak stays closed), *.md excluded.
- Invoice: WS footer + 'Brand: WeddingSimplified' custom field added to create_success_fee_invoice.
  Real $250 AUD test invoice generated (open, no charge/email). Branding header is account-level (shared BlackPan).
- WELCOMED THE 4 (live send from support@weddingsimplified.com): Styled By KC, Paul Osta, Divon, JS →
  promoted to partner_status='partner', codes STYLEDBYKC/PAULOSTA/DIVON/JSPHOTOG saved.
- Outreach RE-ARMED: gates .ws-vendor-outreach-APPROVED + .ws-vendor-welcome-APPROVED created;
  com.ws.vendor-outreach (daily 10:30) + com.ws.vendor-welcome (20min) + reply-monitor (20min) all loaded.
- Internal dashboard PROTECTED HOME: pp-webhook.../internal/dashboard?pw=ws-admin-2026 and
  /internal/pipeline?pw=ws-admin-2026 (live regen) — both pw-gated. /affiliate/admin was OPEN on the
  tunnel → now pw-gated too. Stale "182 drafted/blocked" section in dashboard.html replaced with a live
  "Open Vendor Partner Pipeline" link.
- Backup of pre-portal live files at .pre-portal-backup-20260616-211156/.

⚠️ KNOWN GAP — bride↔vendor messaging loop NOT yet end-to-end in production (does NOT block vendor-facing launch):
- The interactive loop spans TWO hosts: bride side = PocketPlanner (Fly app pocketplanner-ws-autumn-acorn-9932,
  /data/companion.db) AND a local Mac uvicorn (com.agency.pocketplanner, 127.0.0.1). Vendor portal reads the
  MAC companion.db (5 sessions, 0 enquiries, 0 partner_messages). NO sync verified between Fly /data/companion.db
  and Mac companion.db, and vendors.db (partner_invoices) exists ONLY on the Mac (not Fly).
- app.py changes (POST /api/confirm-vendor, GET /api/booking-prompts, GET /api/messages with bride-side masking)
  are written + compile but were NOT deployed to Fly — they lazily import partner_data (sys.path parent) which
  isn't in the Fly build, and hit the cross-host DB split. Deploying as-is would 500 those NEW endpoints.
- NEXT (before any bride enquiry must surface in a portal — weeks of runway): decide canonical PP host +
  companion.db, make the portal read the same DB the live PP writes (or sync), give Fly access to vendors.db
  (or move the invoice trigger to a Mac-side agent that watches for bride-confirmed bookings), add partner_data.py
  + qr_svg.py to the Fly Dockerfile, THEN wire the bride-facing "Have you booked them?" UI nudge.

--- WS partner-portal finishing pass [2026-06-16] ---
Completed 3 features IN .fly-portal-staging/ (Fly PocketPlanner app, companion.db):
1. 90-day persistent login cookie (ws_partner): make_cookie/verify_cookie in partner_dashboard.py (reuse make_token HMAC, ttl=7776000). app.py /partner/dashboard sets cookie on valid magic token; renders from cookie when no token; /partner/login bare redirects to dashboard if cookie valid; /partner/logout clears it. "Log out" link in dashboard footer. HttpOnly/Secure/SameSite=Lax/Path=/partner.
2. Lifecycle emails actually fire: send_lifecycle(kind,partner,ctx) in app.py renders vendor-lifecycle-templates.json (copied into staging + Dockerfile COPY) and sends via ws_gmail_send. new_lead fires in submit_enquiry via _fire_new_lead; booking_confirmed fires in /api/confirm-vendor via _fire_booking_confirmed. Founding guard: vendor_confirmed_booking_count==0 -> founding_line; booking_confirmed fee_line keyed off invoice.status=='waived' so "first booking on us" NEVER repeats. All best-effort try/except + push_kip alerts.
3. Vendor self-service profile: vendor_profiles table (companion.db) + photos on Fly volume /data/vendor-photos/<vid>/. partner_data helpers get_profile/set_blurb/list_photos/add_photo(10 cap+image validation)/delete_photo/photo_path/has_profile. Dashboard "Your profile" card (blurb textarea + thumbnails+delete + upload). Routes POST /partner/profile|photo|photo-delete (token-auth, 303 back), GET /partner/photo/{vid}/{name}, GET /api/vendor-card/{vid}. Bride card: _attach_vendor_profiles enriches chat vendors with blurb+photos, STRIPS website/instagram (in-platform only); index.html showVendorCards renders photo strip + prefers blurb. Welcome template over-promise replaced with self-serve instruction.
py_compile passes; Fly-like import smoke test (COMPANION_DB_PATH temp, no vendors.db) passes. NOT deployed — human reviews + deploys. Added python-multipart to requirements.txt for uploads.

--- 2026-06-16 ~22:20 UTC — WS PORTAL CLOUD-CONSOLIDATION DEPLOYED LIVE ---
Deployed the full cloud build to Fly app pocketplanner-ws-autumn-acorn-9932 (version 11). PocketPlanner + portal now share ONE cloud DB (/data/companion.db).
- Portal serves from the Fly app: weddingsimplified.com/partner/login → 302 → pocketplanner.weddingsimplified.com.au/partner/login (Fly). /partner recruit page intact. _redirects repointed off the Mac tunnel.
- LIVE features: 90-day login cookie (no magic-link-every-time); lifecycle emails firing (new_lead on enquiry, booking_confirmed on bride-confirm) + Kip Telegram alerts, first-booking-free guard via vendor_confirmed_booking_count; vendor self-service profiles (blurb + up to 10 photos at /data/vendor-photos) → bride-facing cards in PP with NO external links; invoices+partners in cloud companion.db with WS footer/Brand custom field.
- Fly secret PARTNER_UPSERT_KEY set (value in /tmp/.partner_upsert_key). 5 partners migrated into cloud `partners` table via /api/partner-upsert: Styled By KC, Paul Osta, Divon, JS + Daniel test partner (daniel@goslingproductions.com). Magic-link sent to Daniel to check the dashboard.
- Verified: PP up 200 (both URLs) post-deploy; /partner/login 200; vendor-card endpoint reads cloud. Deploy WARNING "not listening 8080" was a transient rolling-deploy timing blip — app bound fine.
- Source synced: .fly-portal-staging → planning-companion (canonical). Backup at .pre-portal-backup-20260616-211156. Rollback: `fly deploy` prior release or `fly releases`.
- Mac delivery-server still serves /partner at pp-webhook (now orphaned/unreferenced) — can unload com.weddingsimplified.delivery's portal role later; left running (harmless).

--- 2026-06-16 ~23:10 UTC — WS portal polish round 2 (deployed) ---
- Enquiries table: split into explicit "Lead date" + "Wedding date" columns (was buried under the name). partner_dashboard.py + Fly redeploy (v14/15).
- Round branded file-upload button: fixed (CSS was on login block, moved to dashboard block).
- Test dashboard re-seeded (vendor 9001 / daniel@goslingproductions.com): 6 couples across stages; Grace & Noah now a 12-message thread (scroll test); Chloe & Jack $250 invoice now points at a real WS-BRANDED Stripe invoice (footer "WeddingSimplified · ... a BlackPan Agency brand" + custom field Brand=WeddingSimplified); Mia & Ethan waived founding invoice.
- Bride-view (PocketPlanner) opens via ?s=<session_id>: e.g. /?s=t-mia, /?s=t-priya.
- Org chart (ESC Tier 1A visual) iterated to: Producer → Director | PM; PM → Production Coordinator → 3-way split (Stage Manager+crew | Company Manager+Chorus | FOH+Box Office/Ticketing/Ushers/Bar). Production Electrician removed. Solid bg + SVG splits + bulletproof indented-tree connectors; screenshot-verified all lines connect. File: easystagecraft/course/visuals/production-org-chart.html.

--- 2026-06-16 ~23:50 UTC — WS Anthropic workspace + central admin + chat guard ---
- Anthropic: PP credit-too-low took the bride chat offline (confirmed via Fly logs: 400 "credit balance too low"). Daniel created a "WeddingSimplified" Anthropic WORKSPACE (wrkspc_01VLQNbW9BTJhgxDTN8SXCwZ) + key; set as PP Fly secret ANTHROPIC_API_KEY so WS spend is tracked separately. Daniel topped up $20 (ORG-WIDE — workspaces share one balance, only give tracking + spend LIMITS). PP chat verified live again (200, real reply). TODO: set a per-workspace spend LIMIT in the Anthropic Console to ring-fence.
- Claude Max can NOT power PP (Max = chat app + Claude Code only; an app needs the pay-per-token API).
- Central PARTNER ADMIN deployed: GET /admin?key=<WS_PARTNER_UPSERT_KEY> (ADMIN_KEY unset → falls back to PARTNER_UPSERT_KEY). Lists all partners + totals (leads/bookings/invoices//profile/last-active); each name deep-links to that partner's portal. Key now persisted in agents/.env as WS_PARTNER_UPSERT_KEY (was only /tmp).
- Chat credit-guard deployed: fallback Anthropic call now wrapped — on credit/API error the bride gets a warm hold message (no 500) + Daniel gets a deduped Telegram alert (~1/30min). Verified chat still 200 on happy path.

--- 2026-06-16 ~00:40 UTC (17th) — WS vendor list: enrich-first + Brisbane + blocklist-in-scraper ---
- Decision (Daniel): enrich the 1,088 no-email vendors BEFORE scraping new ones (gets us to ~2,400 contactable). Keep wave to hotspots; ADDED Brisbane → DEFAULT_CITIES now [Melbourne, Sydney, Brisbane], auto-widen to other states later (1,890 already contactable AU-wide: QLD434/VIC431/NSW393/WA220/SA200/ACT140/TAS72).
- Built enrich-emails.py (WeddingSimplified-Delivery-v2/): fetches each vendor's website (+/contact,/about) → extracts best email (prefers same-domain + hello@/info@), junk/placeholder-filtered (mysite.com/wix/example/etc.), blocklist-aware, resumable (only no-email rows), writes email + source '+enriched' so they flow into the wave. python3.13 has requests. Test 15 → 6 found (~40%). FULL run (1,082) launched in background.
- Blocklist (Redron Media / Conrad Hendricks) now also enforced in the scraper: vendor-scraper.py save_vendor() returns None for blocklisted (name/email/website match). Already in vendor-outreach-sender.py due_rows. TODO: wire into vendor-finder.py too IF that path is ever reactivated.

--- 2026-06-17 ~00:50 UTC — WS outreach DELIVERABILITY audit + hardening ---
Daniel asked to make sure the wave isn't getting spam/DKIM-banned. Audit + fixes:
- DKIM: weddingsimplified.com HAS a valid Google DKIM key published (google._domainkey) → outbound from support@weddingsimplified.com signs + DKIM-aligns → DMARC passes via DKIM.
- SPF: was 'v=spf1 include:_spf.mx.cloudflare.net ~all' (Cloudflare only — WRONG, we send via Google). FIXED via CF API → 'v=spf1 include:_spf.google.com include:_spf.mx.cloudflare.net ~all'. SPF now aligns too.
- DMARC: p=none (monitoring, rua→admin@blackpanagency). Fine for now; can tighten to quarantine later.
- List-Unsubscribe + List-Unsubscribe-Post=One-Click headers ADDED to send_email (Gmail/Yahoo bulk-sender best practice + compliance; reply-monitor already processes 'unsubscribe').
- HARD-BOUNCE SUPPRESSION added to vendor-reply-monitor: NDRs (mailer-daemon/postmaster/undeliverable/550/etc.) now parsed for the failed address → vendor set partner_status='bounced'. Sender due_rows NOT IN now includes 'bounced' + 'excluded' → dead addresses never re-emailed (protects reputation = #1 ban-avoidance).
- Volume: ramp 20→40→80/day, well under Google 2k/day; paced.
- EMPIRICAL: 61 emailed → 6 genuine replies (~10%) = emails landing in inboxes, not spam.
- VERIFY (manual, definitive): open a sent outreach email in Gmail → Show original → SPF/DKIM/DMARC = PASS; or run mail-tester.com from a browser (couldn't headless it — JS + bot-blocked).

--- 2026-06-17 ~01:10 UTC — ESC deliverability parity + reply-count clarification ---
- "6 responses" = original first-wave replies (4 YES: Paul Osta/Styled By KC/Divon/JS + Wild Romantic logged twice replied→declined). NOT new since tonight's restart — new wave too fresh.
- WS auth: SPF fixed (now incl Google), DKIM published, DMARC live, ~10% reply rate = inboxing. Automated round-trip verify inconclusive (forwarding strips auth headers; same-org sends skip stamping; Gmail blocks Playwright login). Definitive check = Daniel "Show original" on a received copy.
- ESC: easystagecraft.com DNS ALREADY correct (SPF incl Google, DKIM live, DMARC). Added List-Unsubscribe + List-Unsubscribe-Post to esc-cold-email-sender.py (parity). GAP before ESC sends: no ESC reply/bounce-monitor yet (WS has one) — build before the 'send it'.

--- 2026-06-17 ~02:00 UTC — OVERNIGHT ESC+WS AUDIT + FIXES (Daniel asleep, autonomous) ---
Daniel: full ESC audit (all apps/links/payments/permissions/seats/school/cross-nav/T&Cs) + check WS content posting; find holes, fix them, brief in the morning. Ran 5 parallel read-only auditors.

SHIPPED (autonomous fixes):
- WS homepage → Option C positioning live ("21 years of event experience, guiding every decision") — hero + <title> + og/twitter. Deployed CF Pages, verified on apex. Dashboard leak stays closed.
- WS PINTEREST UNBLOCKED: 10 approved Week-1 pins (launch-batch-week1/png/PN-*) copied to WeddingSimplified-Delivery-v2/social-graphics/ + inserted into sarah.db pinterest_pins (pinned=0) with titles/descriptions/boards parsed from REVIEW.html. Poster works (posted 06-12→15, stalled 06-16 on EMPTY queue). Resumes on next com.agency.sarah cycle.
- MONEY-IN MONITOR + CENTRAL LEDGER: built agents/esc-sales-monitor.py (polls Stripe paid invoices + checkout sessions read-only → esc-orders.db with brand/product/tier/amount/PO/invoice → Kip alert per sale). launchd com.agency.esc-sales-monitor (every 1200s). Tested: 10 orders backfilled, PO 'TEST-001' captured. Closes ESC §5 (no central tracking) + §6 (no money-in alert / reconciliation).
- ESC reply/bounce-monitor: built agents/esc-cold-email-reply-monitor.py (parity w/ WS; reads admin@blackpanagency for daniel@easystagecraft.com replies → sets replied/unsubscribed/done in esc-cold-email-state.db; bounce suppression). No-ops until campaign DB exists. + List-Unsubscribe added to esc-cold-email-sender.py.
- WS deliverability (earlier): SPF fixed (+google), List-Unsubscribe, hard-bounce suppression in vendor-reply-monitor; sender NOT IN now excludes bounced+excluded; Brisbane added + all-states ("ALL") wave.
- Vendor enrichment running (enrich-emails.py bg) → ~2,045 contactable (final count pending).

AUDIT VERDICTS: ESC commerce ✅ (card+invoice+PO+webhook all live, prices match) · infra ✅ (webhook live+secret, KV bound, sites resolve) · permissions/seats/SSO ✅ (team invites + entitlement inheritance + cross-domain SSO real) · site/links: legal pack complete, 3 dead links on secondary pages · WS content: Pinterest fixed, IG/TikTok blocked.

DECISIONS QUEUED FOR DANIEL (morning brief HTML: memory/MORNING-BRIEF-2026-06-17.html; sent to Kip):
1. IG checkpoint(403 since~06-11) + TikTok captcha — manual clearance (his phone). 2. EasyStage app has NO entitlement gate (open via direct URL) — add 'es' gate or intentional? 3. Suite "coming soon" (canonical/Stripe) vs LIVE pricing buttons — confirm. 4. EasyRisk $19(brief) vs $49(live/canonical). 5. Subscription→KV webhook (no customer.subscription.* handler; app-sub access relies on per-load live Stripe lookup that fails silently → lockout risk) — needs Worker redeploy, recommend. 6. Two content systems (sarah.db Playwright [working] vs social.db cron [failing/corrupt rows]) — retire redundant. 7. 3 dead links: easyrisk nav /consulting/ 404, account esc-ohs-framework.html 404, account risk/#standards missing anchor — build pages or remove. 8. Schedule content gen (~$0.47/batch). 9. alt-domain .com.au/.au/.app no 301.
NOTE: apps/permissions audit stalled once (watchdog) → re-ran focused; all 5 complete. ESC cold-email STILL HELD (no send) per Daniel.

--- 2026-06-17 ~02:20 UTC — enrichment DONE +231 (2,187 contactable); Pinterest fail-backoff cleared, posts next auto cycle (verify AM) ---

--- 2026-06-17 — Deputy-replacement rostering spec captured ---
Daniel voice-dumped the Deputy use case for the new EasyScheduler staff-rostering module (chargeable to schools / Caulfield Theatres).
- Core: roster theatre staff across locations + timesheets→approve→CSV payroll export (award mapping already automated in Deputy; Daniel to export format).
- WEDGE Deputy can't do: per-shift GL (general-ledger) code assignment on micro-shifts (one staffer's micro-shift may bill to different GLs across the school). This is the reason a school pays us.
- Locations: Memorial Hall (sub-areas: technician/supervisor/general hand/etc) + Cripps Centre (other Caulfield campus). Admin cross-sees all; employees see own/location.
- Roster types: individually-assigned AND broadcast/open ("click to take").
- Staff portal: clock on/off, see schedule, request shifts, decline/swap.
- Their cost: $9.70/user × ~40 users (~$388/mo) — what we displace.
- Build approach: Playwright tour of Deputy backend → map data model/workflows → rebuild clean inside EasyScheduler (share Suite auth/Stripe/people), add GL-per-shift layer.
- Awaiting from Daniel: location→role list, Deputy payroll CSV export, Playwright login (he clears 2FA).
Spec saved → memory/project_easyscheduler_staff_rostering_spec.md
Still open: Pinterest publish-flow reports false success (pins not live, reset to unposted) — fix path proposed, awaiting steer vs this build.

--- 2026-06-17 (cont.) — EasyRoster v9 big build (Daniel driving, autonomous) ---
EasyRoster preview = https://esc-roster-preview.pages.dev (CF Pages project esc-roster-preview; source easystagecraft-app/scheduler/roster.html; localStorage key esc_roster_v9). Deploy via: cp → scripts/scan-for-secrets.py → wrangler pages deploy. Always node --check the last <script> block + confirm </body> present (dropped-tag bug happened once).
This session shipped: area×day grid (default) + custom pay-period window + group by area/person/DATE + live staff tally sidebar; 3-bit tiles (time·name·note, no GL); parts-based booking popup (person dropdown w/ OPEN-broadcast + OPEN-needs-approval + names + ✎custom; class-from-person readonly; per-part area/start/end/GL/staff-note; +Add-extra-shift chains +3h; Duplicate ×N → open copies); CGS award engine (verified to cent, multi-class + add-custom L4.4); Personnel (payroll ID + award-class-OR-flat-rate + EMAIL + PHONE + venue alloc + import-crew); Timesheets/approval (admin-only, tabs All/Pending/Approved, grouped by the same group-by dim, sched→actual override w/ live recompute + toast, round 15m/1h, editable GL column, manager-note→export hidden from staff, approve LOCKS shift in schedule); staff portal preview (magic-link login note; accept/reject/swap; OPEN broadcast=Take vs needs-approval=Request; clock on/off); loud admin alert bell; Settings (custom field labels + locations w/ INLINE area editing, no popup); light/dark toggle (dark default, theme via CSS vars); Admin vs Employee access levels (employee=staff portal only, $5/head); Publish→email stub (counts staff w/ email); drag-and-drop move shifts; Deputy-format CSV export(+note). Toured Deputy approve/schedule live via cookie session for parity.
Architecture: standalone OR Suite tab (MODE flag); container = CF Pages + Worker (/rosters KV) + entitlement; multi-tenant per-org. Award oracle = Caulfield Theatre Pay Chart PDF (MA000076). Spec: memory/project_easyscheduler_staff_rostering_spec.md.
NEXT (Daniel to review/steer): multi-select bulk edit · AI 'create N shifts' command (needs server-side Anthropic key) · cloud/multi-tenant (/rosters KV + entitlement = sellable) · email/push delivery (PWA web-push recommended over native app) · wire as real EasyScheduler tab + 'Send to EasyRoster'.
STILL PENDING (pre-roster): Pinterest poster false-success (pins not live, 10 PN reset); odd Kip PNGs in Downloads (Daniel said later); 2nd Stripe key 51R4GFn0 confirm revoked.

--- Pinterest auto-poster bug FIXED [2026-06-17] ---
Root cause: agents/sarah.py _post_pinterest_pin_async clicked Publish before the S3 image
upload finished (only waited networkidle+5s). Publish silently no-ops if image bytes aren't
fully uploaded — no error/toast/redirect — and the old code returned True regardless, marking
pins pinned=1 while NOTHING reached Pinterest. That's why Daniel saw "no pins active".
Fix (no commit): (1) wait for "Add alt text" button to become enabled = upload truly done
(90s timeout) before typing/publishing; abort if never confirmed. (2) REAL success detection:
poll up to 60s for a live pinterest.com/pin/<id> URL (redirect or anchor); return False if none.
Verified 2 live pins: PN-04 https://au.pinterest.com/pin/1116189088931581012 ,
PN-05 (via fixed sarah.py) https://au.pinterest.com/pin/1116189088931581202 . Both marked pinned=1.
Also found: account has ONLY ONE board "WeddingSimplified" (user weddingsimplified6); the board
names in the pin queue (e.g. "Moody Wedding Aesthetic 2026") don't exist, so all pins post to the
default board. PN-01..PN-03 were marked pinned=1 by the old broken code — likely phantom (never
actually posted); worth re-queuing if Daniel wants them live. Remaining PN-06..PN-10 safe for the
launchd agent (3/day) to post on its normal cycle with the fix.

--- SESSION CLOSE 2026-06-17 22:00 [auto — idle 2h] ---
Session ended without manual sign-off. Check archives for last active snapshot.
Next session: review session-log.md and pick up from last in-progress items.

--- 2026-06-17 — WS $99-only cutover + commerce cleanup (Daniel approved) ---
WS pivoted to SINGLE product: WeddingSimplified $99 one-time (PocketPlanner = AI feature inside it; Sarah = its persona). $27 Core / $67 Deluxe DROPPED. Brief updated (brief_LOCKED_ws.md CANONICAL section 2026-06-17).
STRIPE (done via API): deactivated 3 obsolete payment links — $27 Core (plink_1TV3Mo...), $67 Deluxe (plink_1TV3Mv...), old $99 PP dup (plink_1TV3Mj...); archived 3 products (prod_UU1PDTS89hcxtv $27, prod_UU1PlXsTb3iQZz $67, prod_UU1P3f0fhtczvo old PP). ONLY active WS link now = $99 (plink_1TiCCO / buy.stripe.com/6oUfZj43f5rhh2X2iQdIA0L → prod_UhbPuUSIdM2Us9 / price_1TiCCN $99 AUD). Other active links are ESC/EO/EasyRisk (not WS).
SITE: index.html + app-upsell.html already $99-only (earlier agent). Stale Core/Deluxe copy in terms/faq/account/thank-you + delivery-server.py old-PP-link being fixed by agent + redeploy via safe-ws-deploy.sh.
delivery-server.py (com.weddingsimplified.delivery, Mac) NOT unloaded — kept for past bundle buyers; its stale PP buy-link fixed to /checkout. With $27/$67 links dead it now gets no new bundle webhooks (idle). PP webhook lives on Fly.
Statement descriptor = BLACKPAN AGENCY (single Stripe acct) — flagged to Daniel, left as-is.

--- 2026-06-17 — EasyRoster PH-states + AU-dates rollout (deployed) ---
EasyRoster (esc-roster-preview.pages.dev / roster.html): added AU_PH_2026 dataset for all 8 states/territories (VERIFIED from official gov sources by agent — VIC/NSW/QLD/SA/WA/TAS/ACT/NT, with holiday names). Settings has a STATE dropdown (state.phState, default VIC) → applyStatePH() loads that state's holidays as object {date:name}. phMap()/phName() helpers; publicHolidays migrated array→object. dayTypeOf uses phMap. PH names show: gold ★+name on grid day-headers, 🎌+name on date-group headers (timesheets/list). PH pay rates ($73.59 TA/$76.42 TT) auto-apply — verified King's Bday 08/06 → $147.18 for 2h TA; QLD switch moves it to 05/10. Dates render AU DD/MM/YYYY (fdate/ddmm). PH list editable in Settings (YYYY-MM-DD = Name). 
CAVEATS (per data agent): VIC AFL-GF-Friday 25/9 + WA King's-Bday 28/9 are provisional until proclaimed; part-day hols (SA/NT Xmas Eve/NYE) treated as full days; regional variants (WA Pilbara, QLD/TAS regional shows) excluded. Anzac Mon-substitute 27/4 only NSW/ACT/WA.
EasyRoster remaining big step: cloud/multi-tenant (/rosters KV + entitlement on auth Worker = sellable) + PWA web-push + wire Digital Dan to Claude via cloud. Needs the live ESC auth Worker (do WITH Daniel).

--- EasyRoster cloud + sales [2026-06-17] ---
DONE (verified live):
- auth Worker (agency-auth) redeployed with additive /rosters endpoints (GET list, GET/PUT/POST/DELETE /rosters/{id}), account-scoped in SCHEDULES KV under ROSTER:{account}:{id} + ROSTERLIST:{account}. Reused existing SCHEDULES binding = zero new bindings = purely additive. Version b98c111d.
- Smoke-tested post-deploy: existing routes (/schedules /people /entitlements /shows) still 401 no-session (unbroken); new /rosters 401 (live+gated); CSRF 403 enforced; invalid-id 400. app.easystagecraft.com already in CORS whitelist → credentialed sync works on app domain.
- roster.html wired with hybrid localStorage<->cloud sync (cloudPull on boot + visibilitychange, debounced cloudPush on save, last-write-wins by _savedAt, ROSTER_ID='main'). Boots clean (Playwright, no fatal errors). Degrades to local-only when signed-out/off-domain.
- Built EasyRoster sales page easystagecraft-app/easyroster/index.html (suite dark branding, feature grid, award-engine demo, 3-tier pricing chart Free/$5-per-person/Suite-included). Deployed to PREVIEW sales.esc-roster-preview.pages.dev (NOT live).

BLOCKED / NEEDS DANIEL:
- PRICING decision (M3): proposed $5/person/mo (his earlier figure; Deputy ~$9.70/user). Need confirm before minting live Stripe links.
- SAFETY FINDING: local easystagecraft-app/ working tree is BEHIND live for 6 shell pages (account/admin/faq/index/login/shows.html — live larger/newer). A blind full-dir `wrangler pages deploy` to app-easystagecraft would REGRESS live pages. Must reconcile (pull live shell pages into staging) before promoting roster.html + /easyroster to the live app domain. Did NOT gamble.

NEXT (on Daniel's pricing confirm):
- Reconcile staging dir (live shell pages + new roster.html + easyroster/ + _redirects /easyroster), deploy to app-easystagecraft.
- Create Stripe product + payment link for EasyRoster at confirmed price; wire .buy[data-buy] button.
- Add EasyRoster to Suite nav + entitlement.

--- EasyRoster awards + decisions [2026-06-17 cont.] ---
Daniel decisions: ER included in Suite for now (standalone $5/person/mo = future PAYG, shown coming-soon). Roadmap: MYOB/Xero payroll export.
Built: per-person pay dropdown in Personnel (CGS in-house / MA000076 / LPA / Custom hourly) replacing award/$hr toggle. Custom hourly has base + OT + DT fields via synthetic rate-class through the tier engine. Verified locally (Playwright): LPA 7 grades load, optgroups render, award + custom-OT/DT costing both correct.
Award libs: sub-agents compiling awards/lpa.json (DONE, 7 PS grades, eff 2025-07-01, penalties part-unverified) + awards/ma000076.json (agent running). Loader merges same-origin on boot; unverified award rates flagged with confirm-before-payroll banner in rates editor; editing a row clears the flag.
Sales page scrubbed: removed Memorial Hall/Cripps (private venue names) → generic "your theatre, your venues, your spaces"; pricing reframed Suite-inclusive + $5 coming-soon; roadmap line added.

--- EasyRoster LIVE + Stripe + invoice + award rules [2026-06-17 cont.2] ---
WENT LIVE on app.easystagecraft.com (full ESC app redeployed; verified existing apps scheduler/risk/sm/inventory/login all still 200 — zero regression. Earlier "stale local source" fear was FALSE — diffs were Cloudflare edge-injection only, confirmed by normalising CF artifacts).
- Sales page LIVE app.easystagecraft.com/easyroster/ ; app LIVE /scheduler/roster.html (cloud sync works for logged-in ESC users — app domain is CORS-whitelisted, preflight passes).
- STRIPE minted (acct_1PQh97 BlackPan Agency AU): product prod_UilDwHtZSgnm8r, price price_1TjJhCS3wE7niBWKWfiyAhkH ($5/person/mo AUD per-unit), card payment link https://buy.stripe.com/8x28wRbvH4nd4gb5v2dIA0M (adjustable qty). Daniel reversed standalone from "future" → SELLABLE today.
- Pay-by-invoice: scripts/easyroster-invoice.py (dry-run default). send_invoice sub, billed monthly in advance + 1 extra month upfront buffer (first invoice=2x) so access never lapses. Emails to client AP email; recurring PO as customer-level Stripe custom field on every invoice. Net-30. Sales-page mailto collects org/seats/AP-email/PO.
- 4 award libraries compiled+loaded (75 classes/76 options): MA000076 Schools(17), MA000080 Amusement(11), MA000081 LPA(7), MA000091 Broadcasting/Film(38). Per-award RULES mapped into engine (OT span/min-call/break/meal/Sat treatment); CGS keeps legacy rules.
- MA000076 = Educational Services (Schools) General Staff (NOT amusement — that's MA000080). CGS $33.45/$34.74 = casual rates. CGS afternoon-flat = legacy CGS rule award lacks.
FAST-FOLLOW (noted, not blocking): entitlement-gate /rosters sync to paid customers (currently any logged-in ESC session syncs — soft launch); cross-link EasyRoster in other Suite apps' nav.

--- EasyRoster gate + trial + privacy + Sarah comp [2026-06-18] ---
- Entitlement gate HARDENED + deployed: /rosters WRITES require 'rost' tier (admin/comp/Suite/Stripe-buyer); reads stay open (account-scoped). Verified: existing routes intact, writes 401 w/o session, comp-onboard secret+CSRF gated.
- Added reusable /admin/comp-onboard endpoint (gated by COMP_MINT_SECRET wrangler secret) → mints custom-TTL magic link + emails it. Use for all future comps.
- COMP: Sarah Butler (Kalinda Primary, sarah.butler@education.vic.gov.au) granted SINGLE-SEAT full course (1A/1B/2) + full Suite (eo/ei/er/es/rost). 48h magic link EMAILED (comp-onboard 200 ok). Added to COMP_COURSE_TIERS. Besen comp also got 'rost'.
- 'rost' = EasyRoster entitlement key. Added to ALL_TIERS (Suite grants it), appKeysFromNickname (easyroster→rost for Stripe buyers).
- PRIVACY: genericized roster.html seed — removed Memorial Hall/Cripps/real staff/Matilda → Main Theatre/Studio Theatre/generic staff/Spring Showcase, rate group "House rates (example)". Daniel's real CGS data lives in his cloud sync, unaffected.
- TRIAL: non-entitled users get 30-min generic sandbox; NO export, NO send/publish, NO persistent save (in-memory only); 30-min wall → Subscribe $5 / Sign in. Paid/comped/admin unlocked (checkEntitled via /entitlements). Verified locally.

=== FLAGGED FOR TOMORROW (Daniel, 2026-06-18 — do NOT start tonight) ===
1. EasySM — one more tool/feature from STAFF FEEDBACK. Daniel to provide the detail.
2. EasyRisk — addition for SCHOOL risk assessments for kids, including catching buses / excursions etc.
Both deferred to tomorrow by Daniel; flagged here so not lost.

STILL QUEUED (EasyRoster, from earlier tonight, not yet built):
- Minimum REST break between shifts (10/12h) + roster alert when approaching + auto double-time on next shift if breached.
- Clock in/out rounding + break auto-adjust (actual times → recompute time+cost with rounding).
- Cross-link EasyRoster into other Suite apps' nav bars.
- 2-tier pricing: Lite $5 (rostering) / Pro ~$9 (award auto-interpretation + payroll export) — Daniel approved the idea.

--- SESSION CLOSE [2026-06-18 ~00:35 AEST] ---
Autonomous overnight build (Daniel → bed, "work through all builds"). ALL deployed live + verified; existing Suite apps confirmed intact (no regressions).
Completed tonight:
- EasyRoster: rest-break rule (10/12h → next shift double-time + ⏰ tile flags), clock rounding (15min, auto-recompute), per-award RULES mapped (OT span/min-call/break/meal/Sat per award), FT/PT employment types + HR·Leave tab (annual+personal accrual, casual hours, rest-breach/fatigue counts).
- Privacy: genericized seed (no Memorial Hall/Cripps) + 30-min no-export/send/save trial sandbox + wall.
- Suite chrome added to roster.html (was orphaned) + EasyRoster added to suite-shell app-switcher (all apps).
- Entitlement gate hardened (writes need 'rost'); comp-onboard endpoint; Sarah Butler comped + 48h link emailed; pay-by-invoice (AP email + recurring PO + 1mo buffer).
- 4 award libraries (Schools/Amusement/LPA/Broadcasting, 76 options).
- EasyRisk: 26-row child-safe Schools Excursion RA template + sidebar (via sub-agent, reviewed + deployed).
- 2-tier infra: Pro $9 price+link created (price_1TjKUwS3wE7niBWKU5PHPupH); sales page NOT flipped (awaiting feature-split decision).
- Morning brief written.
DECISION-GATED (in morning brief, need Daniel): 2-tier feature split; EasySM "book" sketch; casual-loading +25% toggle; Schools RA wording review.
NOT done (need input/low-value): EasySM build (needs sketch), FT/PT→EasyRisk fatigue board report (next step), footer/support-email/marketing-story polish (audit 🟡).
No blockers, no API-zero, no failures — no overnight Kip ping warranted; morning brief covers it.

--- NOTE [2026-06-18 06:00] ---
STRUCTURAL GAP: the nightly-digest agent (com.agency.nightly-digest, 06:00) REGENERATED kip-morning-brief.md and clobbered the hand-written EasyRoster overnight summary. Re-inserted the EasyRoster section at the top of the regenerated brief. PROPER FIX (for Daniel/next session): digest generator should ingest a separate 'overnight-build-notes.md' file and prepend it, OR write autonomous build summaries to a non-clobbered file. Until then, hand-written brief content must be re-added AFTER the 06:00 digest run.
Also observed: an unattributed background task "Find chat endpoint + capture recent Fly error logs" (bd2wwbcn2) completed exit 0 — not launched by this session; left untouched (no context, not blocking).

--- 2026-06-18 (Daniel back) ---
- Casual-loading +25% toggle: per-person checkbox in Personnel ("+25%"), defaults ON for casuals/OFF for FT-PT. Applies ONLY to _ordinaryBased award-library classes (not CGS/house/custom). Verified ×1.25. Deployed live.
- EasyInventory HOME STATE built (via sub-agent): landing layer with large central "What are you looking for?" search + per-category summary tiles (e.g. Costume — N catalogued); logo=Home; tile click→category filter, search→cross-category; default view on load. node-checked. Deployed + verified live.
- Schools RA reviewed via screenshot — 27 rows render well behind EasyRisk liability disclaimer; reads professional. Daniel to review wording at app.easystagecraft.com/risk/ → sidebar "🏫 Schools Risk Assessment".
- Presented 2-tier feature-table proposal (awaiting Daniel pick). EasySM book: Daniel looking for a reference image before build.

--- 2026-06-18 — 2-TIER LIVE (Daniel approved the split) ---
- Entitlements: 'rost'=Lite, 'rost_pro'=Pro. ALL_TIERS + full-Suite comps (Sarah/Besen) get rost_pro. appKeysFromNickname: easyroster→rost; "easyroster pro"→+rost_pro. Worker deployed.
- In-app gating (roster.html): ENTITLED=any paid (Lite|Pro), PRO=rost_pro|admin. Pro-only = award engine (loadAwardLibraries), HR·Leave tab, Digital Dan. Lite = rostering + house/custom rates + cloud + roles + timesheet/export. checkEntitled sets both + loads award libs if PRO + applyProGating. TRIAL shows full Pro PREVIEW (PRO=true while trial active) so prospects see the value; 30-min wall still gates.
- Sales page (easyroster/): 3 columns — Lite $5 (link 8x28wR…) / Pro $9 featured (link fZu00l…) / Suite Included. Both tiers card + pay-by-invoice. Deployed + verified live (trial preview: PRO=true, award libs+HR+Dan present, signed out).
Stripe: Lite price_1TjJhC / Pro price_1TjKUw (both live).
EasyInventory home state + casual toggle also live (earlier today). Schools RA reviewable at risk/ sidebar.
EasySM 'the book' still awaiting Daniel's reference image.

--- 2026-06-18 — BIZ-DEV LEAD + SM-software context ---
- StageWrite (Open Jar Productions) reviewed = category-leader SM app, 100k+ users, 100+ Broadway shows, listed on MTI marketplace. Core = digital script + cue marking + spacing charts + Apple Pencil blocking. SILOED (no roster/award/risk/inventory). Our wedge = integrated Suite, web, no per-seat Broadway pricing. Borrow later: spacing charts (actor positions per beat) + freehand traffic arrows.
- SM "the book" spec LOCKED from 4 real calling-script photos (Downloads sm2/sm3/sm4/smcopy): centre script column; RIGHT-margin GO cue boxes (LX/SD/SQ/QL/TAB/DECK + number + .5 sub-numbers) w/ leader lines to trigger word; LEFT-margin STANDBY boxes + red warnings; highlighted trigger word; per-dept cue sheets auto-populated + live-linked. Building now in EasySM (sub-agent).
- 🤝 BIZ-DEV LEAD (Daniel): Stuart Hendricks = manager MTI Australia (cousin of Conrad Hendricks, Daniel's EX business partner — sensitive relationship, Daniel to navigate). OPPORTUNITY: collab to PRE-LOAD MTI licensed scripts + a ready-made SM calling-script draft into EasySM for teachers/schools. Extend to other AU rights-holders. WARM CONTACTS Daniel has: Stuart Hendricks (MTI AU), David Spicer (David Spicer Productions). Plus Origin Theatrical, Hal Leonard as targets. DESIGN IMPLICATION: EasySM "the book" should support pre-loaded per-show script TEMPLATES (a template library keyed by show/licensor) — huge distribution + content moat vs StageWrite. NOT acted on; flagged for Daniel.
- 📣 MARKETING CHANNEL (Daniel): David Spicer is ALSO an editor of **Stage Whispers** magazine (~5,000 school teachers reach). Daniel previously promoted ESC there — can do again. Ready-made school-teacher distribution for the whole Suite + EasySM/EasyStage. Flagged for a promo push.

--- 2026-06-18 — EasyStage triangulation + EasySM "the book" BUILT & LIVE ---
- EasyStage TRIANGULATION (the Lily Wieland "most accurate markup" method, automated): reference points A(OP)/B(PS) on the setting line (editable spread default ±2m + ref-line Y); per-mark distance-from-A + distance-from-B; shallow-arc ⚠ (angle<25/>155); printable "📐 Markup chart" (method blurb + A/B defs + scale-check + per-scene tape colour + table) + dA/dB added to CSV; imperial m⇄ft toggle. A/B drawn on canvas (cyan). Verified live (Table C @centre = 4.03m both). easystage/index.html.
- EasySM "THE BOOK" (calling script) — new tab: centre script (paste/.txt/PDF-beta) + click line→right-margin GO cue (LX/SD/SQ/QL/TAB/FLY/DECK/SET/SPOT/FOH, auto-num + .5 inserts) + SVG leader lines + auto STBY left margin + red warnings + cyan trigger-word highlight. Per-dept cue sheets auto-derived + LIVE-LINKED (move cue→row re-sorts; delete cue→row vanishes; manual rows persist). Print calling script + CSV/PDF per sheet. Persists doc.book via existing /sm-data sync. Free-explore gate covers it. easysm/index.html. (built by sub-agent, reviewed + deployed.)
Both deployed to app-easystagecraft + verified (1 structural body, JS OK, no pageerrors, key fns present).
DESIGN HOOK for MTI/Spicer biz-dev: "the book" supports paste/import script → pre-loaded per-show templates are a natural next step (script + starter cue draft per licensed title).

--- 2026-06-18 — Sarah Butler first-look feedback INGESTED ---
Glowing from a self-described non-technical teacher. WINS: Inventory "exactly what I want / idiot proof" (tags + photo upload + 5-trial-items), Schools RA "gasp out loud / slack jawed" (hazard-library breadth), EasyStage "wish I had it 15 months ago" (vs PowerPoint/hand-drawn), Digital Dan "cute clever". Course→Suite cross-sell proven (learned SM role from VO course).
KEY SIGNAL: EasySM "too grand / too much" for a teacher → needs a SIMPLE 'teacher mode' / guided on-ramp (progressive disclosure); pro depth stays for real SMs. Flagged as next EasySM refinement (pairs w/ pre-loaded script templates). Saved → memory/project_esc_suite_user_feedback_sarah.md.
Roster + Scheduler not yet explored by Sarah (no feedback there yet).

--- 2026-06-18 (overnight, Daniel→bed "keep feature building") ---
DEPLOYED tonight: EasyRoster — Settings ✕ close; GL-code-names moved UNDER Field labels + made fully editable (edit code → renames across all shifts; edit name; + Add; ✕ delete with in-use confirm); field-label inputs themed (were white/unstyled — collapsible .set-d .set-body had no input CSS rule); timesheet "Taken" label + Approve column header + GL font fixed; rates editor collapsible-by-award + search + ✕; account-page EasyRoster tile. All verified + live.
RUNNING (background agents, will verify+deploy on completion):
- EasySM "the book" extensions: P1 Worksheets tab auto-populates cue sheets (sync), P2 obvious coloured cue-type selector, P3 PDF full-script import → auto-split scenes into top tabs, P4 sheet-music/image-page mode (click a word OR a note → cue at x/y on rendered page), P5 Simple/Full toggle (default Simple, teacher on-ramp).
- EasyRisk school-focus: prominent "🏫 For schools — start here" affordance + 3 new school RA templates (Production/Performance, Bump-in/out with students, Event/Front-of-house).
STRATEGY captured: project_esc_schools_strategy.md — teachers segment, Simple/Full + school templates (one product not forks), make school tools prominent + course cross-links (course-site edits gated on Daniel's review — don't edit marketing/course blind overnight).
NEXT (after agents land): verify+deploy both, write morning brief. Queued still: per-show script template library (MTI/Spicer pre-loads), FT/PT→EasyRisk fatigue board report, StageWrite-style spacing charts/traffic arrows.

--- SESSION CLOSE [2026-06-18 overnight] ---
ALL deployed + verified live (app.easystagecraft.com), no regressions, agents reviewed before deploy.
Shipped this session:
- EasyRoster: GL codes editable (rename-across-shifts/add/delete) + moved under Field labels; Settings ✕; field-label theming fix; timesheet Taken/Approve/GL-font; rates editor groups+search+✕; account-page tile.
- EasySM "the book" +5: Worksheets cue-sheet sync, coloured cue-type selector, PDF→scene tabs, sheet-music/score page-click mode, Simple/Full toggle (default Simple = teacher on-ramp).
- EasyRisk: "🏫 For schools" prominent entry + 3 new school RA templates (Production 20, Bump 14, Event 15) atop the Excursion 27.
Memory saved: project_esc_suite_user_feedback_sarah, project_esc_schools_strategy.
Morning brief written (kip-morning-brief.md) — NOTE: the 06:00 nightly-digest may overwrite it again (known structural gap); re-prepend the build summary if so.
DECISION-GATED for Daniel: schools strategy (one-product Simple/Full vs forks — recommend one-product); course-site cross-links (NOT edited blind — needs his look); biz-dev MTI/Spicer/Stage Whispers.
NEXT (not started — needs spec/decision or cross-app, deliberately not gambled overnight): per-show script template library (MTI pre-loads); FT/PT→EasyRisk fatigue/TOIL board report (cross-app); StageWrite spacing charts + traffic arrows; polish (footers/support email/marketing story).
Stopped here deliberately: 3 substantial builds shipped+verified; remaining items are decision-gated/cross-app/low-value — quality over speculative overnight builds.

--- 2026-06-20 — ESC build list continued ---
STRIPE CHECK: kFgHGf (19-June flagged sk key) = OLD already-rotated key. Current live key ends 86WcRQ (works, BlackPan Agency). kFgHGf NOT in repo/git-history/disk/live-pages/Worker/functions; no git remote; .env gitignored; Stripe charges/events clean (no abuse); balance $0. Verdict: old leak, remediated, no active exposure. Daniel to glance Stripe dashboard that kFgHGf shows Rolled. Offered optional weekly secret auto-scan.
DEPLOYED + verified live:
- EasyRoster Fatigue & TOIL board report (HR tab, Pro-gated): YTD ordinary/OT(est), rest breaches, longest week, max consecutive days, TOIL balance (OT−taken, editable p.toilTaken), RAG fatigue flags (RED week 60h/consec 10d/2 breaches; AMBER 50h/7d/1), printable board report (printFatigueReport). OT/ordinary split is an estimate (engine returns $ not hours) — labelled (est.).
- EasySM The Book script TEMPLATE library: "📚 Start from a template" picker (#bkTplModal) + BOOK_TEMPLATES array; samples = R&J balcony (public domain) + School Showcase running order + blank starter, all marked demo-only. Licensed MTI/Spicer titles append as data (marked insertion point). bookApplyTemplate reuses bookLoadScript path.
Daniel finalising CONTENT this weekend (RA wording / templates) — frameworks built to drop final content into; samples kept clearly placeholder.
NEXT on ESC list: StageWrite-style spacing charts + freehand traffic arrows (EasyStage has tracks already); school presets across apps (EasyStage school-hall venue, EasyInventory school-store template); polish (footers/support email/marketing-site EasyRoster story).

--- 2026-06-20 — EasyStage school venue presets LIVE ---
Added 4 school presets to easystage/venue-presets.json (top of Venue Vault, before named theatres): School Hall (9.0×5.5, no wings/pit), School Gymnasium end-stage (7.0×4.0), Drama Studio/Black-box (7.0×7.0 flat), Classroom performance space (6.0×4.0). state="" generic, verified=false (typical AU defaults, user-adjustable live). 19 presets total. Deployed + verified live.
SESSION ESC build status: EasyRoster (2-tier+awards+HR+fatigue/TOIL report), EasySM (book+5 extensions+template library), EasyRisk (4 school RA templates + For-schools), EasyStage (triangulation markup + school presets), EasyInventory (home state) — ALL live.
NEXT: StageWrite spacing charts + freehand traffic arrows (needs Daniel spec steer — actor positions per scene vs existing tracks); EasyInventory school-drama-store starter template; polish (footers/support email/marketing-site EasyRoster story).

--- 2026-06-20 — EasyOrchestra health + EasyInventory perf fixes ---
EasyOrchestra: health-checked live (headless) — loads clean, title OK, only expected signed-out 401s (sync attempts, no session) + benign CF-analytics CORS. Structurally healthy. Demo "went weird on login" = most likely STALE CACHE (Daniel's guess) — hard-refresh clears. Offered cache-busting if recurs.
EasyInventory SLOW PULL — diagnosed: cloudPullInitial fetched items ONE-AT-A-TIME (sequential per-item GET) AND each item carried a full 1920px q.85 photo (~0.5-1.5MB). Thousands of items × sequential × big = "forever / stuck on cloud pulling".
FIXES DEPLOYED:
1. PARALLELIZED pull — 12 concurrent fetches (was 1) + progressive reload every ~60 items → ~10x faster.
2. CAPTURE CRUNCH (Daniel greenlit "scale files as taken") — full image 1920/q.85 → 1200/q.78 (~5x smaller/item). Applies to NEW captures; existing large items stay until re-saved (one-time recompress migration available if wanted).
3. CLEAR SYNC ICONS — badge now ⬇️ Downloading / ⬆️ Saving / ☁️ Synced (yellow while active) + progress count X/N.
4. HOME-SCREEN sync line (#homeSyncNote) under the "What are you looking for?" search → "⬇️ Downloading inventory from cloud… X/N".
DEEPER STREAMLINE (for true thousands-scale, NOT yet done — needs Worker change + migration): split thumb vs full-photo storage so the initial pull fetches only 400px thumbs (index/grid) and lazy-loads the full image only on detail view. Removes full photos from initial pull entirely. Flagged for Daniel.

--- 2026-06-20 — NEW SILO: Marriage Simplified + multi-task fan-out ---
Daniel: big new standalone project + several fixes. Launched 4 parallel agents + did foundation:
- ABN for Gosling International = 28 219 744 700 (The Trustee for Gosling International, QLD active) — confirm right entity.
- Karen Gosling persona researched (35+yr marriage+ADHD/ASD counsellor, Gold Coast; education-first, non-judgemental, root-cause, reconnection). Voice-dump questionnaire prepared → marriage-simplified/karen-persona-voicedump.md (for Karen to record once data ready; QLD few-days turnaround).
- CF DNS: zone marriagesimplified.com.au created (pending). NS for Mike: alexia.ns.cloudflare.com + rodrigo.ns.cloudflare.com. (.com.au/.au available; .com parked-for-sale; ASIC name available.)
AGENTS RUNNING: (1) compliance research → marriage-simplified/legal/ (AU Privacy Act/APPs, health/sensitive info, de-id communal model, counsellor-not-therapy, crisis escalation). (2) Marriage Simplified scaffold (Workers+D1, customer chat portal + Karen admin portal + memory + communal-learning model + consent + Gosling Intl branding; persona injection point; NOT deployed, no real data — gated on compliance). (3) PocketPlanner: account log-back-in (email→resend magic link) + rich demo seed (vendors/messages/guests/budget/checklist) for gozdemo. (4) FijiLIFE Monday: diagnose why charity-fund application form responses aren't emailing admin@fijilife.au.
Memory: project_marriage_simplified.md.
COMPLIANCE GATE: Marriage Simplified handles sensitive health data — needs real AU lawyer/privacy sign-off before go-live. Build the frame only tonight.

--- FijiLIFE Monday email-notification diagnosis [2026-06-20] ---
Issue: charity-fund application form responses not emailing Mike at admin@fijilife.au.
Findings via Monday API (token MONDAY_TOKEN, account 33288787 FijiLIFE Foundation, pro tier, sole user Mike Gosling admin@fijilife.au admin+enabled):
- Target board = "Charity Funding Application" id 5027772890 (has charity-specific cols: Fiji Charitable Trust Act reg, FJD funding amount, beneficiaries, project budget files).
- Mike IS subscribed to board with CORRECT email admin@fijilife.au. Recipient address correct, not a typo.
- Form->board WORKS: 2026-06-03 item "Foundation for the Education of Needy Children in Fiji" landed fully populated. Board has a Monday native WorkForm view (FormBoardView 52560453) AND an (empty/unused) JotForm Submission ID column. The 06-03 item came via native WorkForm (JotForm ID blank).
- Only 2 create_pulse events ever (2026-04-12 setup, 2026-06-03 real). NONE since 06-03.
- Monday GraphQL API CANNOT read or edit automations ('automations' field does not exist on Board type) — automation/notification recipes are UI-ONLY. Could not verify or fix the email automation via API.
ROOT CAUSE: cannot be confirmed via API. Most likely the board email-notification automation is missing/disabled, OR going to spam. Form intake itself is healthy.
DANIEL ACTION (UI-only): open board 5027772890 > Automations > confirm/create "When item created, notify admin@fijilife.au" (or email-item recipe); check it's ON; ask Mike to check spam for monday.com sender. Nothing safely fixable via API.

--- FijiLIFE form notifier built [2026-06-20] ---
Built reliable polling notifier replacing Monday's flaky native automations.
- Script: agents/fijilife-form-notifier.py (python3.13)
- Watches BOTH application boards: Charity Funding Application 5027772890 + Individual & Family Applications 5027774771
- Polls Monday GraphQL every 300s via launchd com.fijilife.form-notifier (RunAtLoad true)
- Emails admin@fijilife.au on each NEW item, reusing ws_gmail_send._service() AUTH ONLY (Workspace svc acct impersonating admin@blackpanagency.com.au, gmail.send scope) — composes own FijiLIFE-branded From so WS alias isn't leaked
- State: agents/.fijilife-notifier-state.json, per-board seen_ids high-water mark. First run BASELINES (records existing, sends nothing).
- Baseline run confirmed ZERO emails. Send-path proven via self-test email to admin@blackpanagency.com.au (NOT fijilife). Logs: logs/fijilife-form-notifier*.log

--- 2026-06-20 — ALL 4 fan-out agents DONE ---
1. PocketPlanner: "Email me my link" login (live, deployed) + gozdemo rich seed (8 bookings/8 messages/22 guests/16 budget/28-of-78 checklist, live). pp-cloud deployed version a1e22498. NOTE: PUBLIC_BASE_URL var still legacy pocketplanner.weddingsimplified.com.au (login uses origin-based links so OK; Stripe/relay links use stale var — future fix).
2. Marriage Simplified frame BUILT + validated (tsc clean, dry-run ok) at /Users/kip/agency/marriage-simplified/ — couple portal (magic-link + consent gate 18+/sensitive/communal-opt-in-default-off + chat + memory + crisis), Karen admin portal (/admin, search, transcripts, session notes + communal de-id model), prompt.ts KAREN_KNOWLEDGE injection point, audit log. NOT deployed (gated on Daniel + lawyer sign-off). Deploy steps in README. Model claude-sonnet-4-6 + haiku for memory.
3. Compliance: 5 docs in marriage-simplified/legal/ (COMPLIANCE-CHECKLIST, privacy-policy, terms, consent-and-disclaimer, safety-escalation). Key: full APP bar (health service, no small-biz exemption), express consent, de-id communal, NOT therapy positioning, mandatory AI disclosure + crisis (eSafety penalties up to $49.5M). Needs AU lawyer sign-off before go-live.
4. FijiLIFE notifier LIVE: agents/fijilife-form-notifier.py + com.fijilife.form-notifier.plist (launchd 300s, loaded). Reuses ws_gmail_send AUTH (service account) but FijiLIFE-branded MIME → admin@fijilife.au. Watches BOTH boards: Charity 5027772890 + Individual&Family 5027774771. Baselined (sent ZERO on first run); only future new form items email. State agents/.fijilife-notifier-state.json. No Monday-automation/Gmail-linking dependency.
Confirmed: ABN 28 219 744 700 (Gosling Intl) + domain marriagesimplified.com.au (CF zone pending, NS alexia+rodrigo.ns.cloudflare.com for Mike). Karen questionnaire live at marriage-simplified-questionnaire.pages.dev.

--- 2026-06-20 ~20:05 ---
WS Instagram crisis + returning-customer login + DNS:
- Built IG MANUAL posting pack → https://ws-ig-pack.pages.dev (9 posts, mirrors live Pinterest PN-01..09 exactly per Daniel, $99-aligned CTAs, copy-paste captions, phone long-press save). IG automation stays PAUSED (WS_IG_ENABLED=False) — instagrapi tripped bot-protection → suspension. Safe path = official Graph API.
- WS DNS: BOTH zones weddingsimplified.com.au + weddingsimplified.com now status=active on CF; registrar NS confirmed pointing to CF (sureena/todd + jeff/ursula). Propagated.
- Returning-customer login VERIFIED LIVE: pp-cloud.weddingsimplified.com.au resolves (CF-proxied) + POST /api/login/request returns 200 → magic-link "email me my link" flow working on the real domain.
- Answered Daniel's EasyStagecraft multi-show access-control architecture Q (show-level membership / "assign crew" on show-setup page + membership-filtered show dropdown; person stays single account-level identity for cross-show clash detection). Recommend building show_members + assign-crew panel.

--- 2026-06-20 ~20:25 ---
Returning-customer login UX FIXED (Daniel: "magic link first time only" / land logged-in):
- pp-cloud SPA used ?s=<sid> URL as the only identity → every return w/o link hit email gate. Fix: persist sid to localStorage on first arrival, clean ?s= from URL (history.replaceState), auto-resume from localStorage on bare return; stale stored id self-clears → email gate; added signOut() for shared devices. frontend.ts.
- Playwright-verified (py3.13): link→stored pp_sid=gozdemo, bare-return resumes app, NO gate. ✓ LIVE on pp-cloud.weddingsimplified.com.au.
- Found TWO PocketPlanners: old Fly app (pocketplanner.weddingsimplified.com.au, 66.241.124.254) + NEW worker (pp-cloud.*, CF+D1+AU). Cutover built (CUTOVER.md) but never flipped — needs Daniel + 1 test $99 purchase to verify payment path. Pointed worker PUBLIC_BASE_URL → pp-cloud.* so worker-issued links are consistent. DECISION PENDING: consolidate two apps (flip pocketplanner.* → worker).
- ESC deploys LIVE: EasyRisk collapsible schools group + EasyOrchestra central-map cold-load fix → app-easystagecraft (b83c26a4).
- IG: built agents/ig_graph_publish.py (official Graph API publisher, replaces instagrapi). Daniel has Professional IG + FB WS Page. Remaining: link IG↔Page, create Meta dev app, hand over App ID/Secret + a token → I wire + (likely dev-mode, no app-review needed for own account) auto-post resumes safely.
- Starting: EasyStagecraft show-level access (PM-assign crew, membership-filtered dropdown) per Daniel approval.

--- 2026-06-20 ~20:50 ---
EasyStagecraft SHOW-LEVEL ACCESS CONTROL (PM-assign) — BUILT + DEPLOYED:
- Daniel's scenario: PM owns Production Suite w/ multiple simultaneous shows; a crew member on Show 1+3 must NOT see Show 2+4 in their show menu. Solution = show-level membership.
- Elegant: accountEmailFor(session) already returns team_owner||email, so a crew member's session ALREADY reads the owner's show list. No reverse index needed — just filter that list by viewer email.
- Worker (agents/auth-worker/cf-magic-link.js, agency-auth, deployed de12d59b): shows now carry members[] (emails). handleListShows filters by showVisibleTo(owner sees all; member sees only assigned; empty members = owner-only). handleGetShow/handlePatchShow enforce visibility; members[] edits are owner-only (403 otherwise). New helpers sanitiseMembers/isOwnerViewer/showVisibleTo. List response adds viewer_is_owner.
- shows.html (app-easystagecraft, 7bd7a7b0): "Crew access" tick-list on the EDIT form (owner-only), loads /people (only people WITH email assignable), pre-checks from show.members, sends members[] on PATCH. Each card shows "👥 N crew assigned" / "🔒 Only you" hint for owners. The active-show dropdown (suite-shell) auto-scopes since it builds from filtered /shows.
- Smoke-tested: /shows still 401 w/o auth (routing intact); shows.html loads clean (no JS exceptions). FULL end-to-end (owner vs member visibility) needs a real assigned crew member to confirm — flagged.
- Default semantics: a newly-added crew member sees NOTHING until PM ticks them onto a show. Owner always sees every show.

--- 2026-06-20 ~21:20 ---
CORRECTION + pp-cloud password login:
- I WRONGLY framed WS as "broken / two competing apps". RENDERED both: pocketplanner.weddingsimplified.com.au = LIVE $99 SALES/LANDING page (Stripe checkout, on Fly) — NOT an old app. pp-cloud.weddingsimplified.com.au = the customer APP (magic-link gated, CF Worker+D1, where our recent vendor-mgmt/tools/login work lives). They're sales-page→app, both intentional. WS is NOT broken. Root cause of my error: grepped raw HTML tab-words instead of rendering; mistook marketing copy for app tabs. Did NOT touch DNS/sales page/payments. Cutover ABORTED.
- Daniel confirmed pp-cloud is the canonical app. Wants ONE-CLICK login for existing customers, magic link only for signup.
- BUILT password login on pp-cloud (worker pocketplanner, deployed 5c92da7c): D1 sessions += password_hash/password_salt (PBKDF2 100k/SHA-256). New endpoints POST /api/login/password (email+pw → session_id) + POST /api/account/password (set, session-authed). /api/session/magic returns has_password. Frontend: login gate now leads with email+PASSWORD (one click); magic link demoted to "first time / forgot password"; in-app "set password" header link + one-time nudge sheet; persistent-session (localStorage) still lands same-device users straight in.
- Verified: set-pw ok; correct pw → session_id; wrong pw → 401; browser form login → into app. gozdemo demo is tied to daniel@goslingproductions.com (temp pw weddingtest123 set for his testing).
- OPEN (needs Daniel confirm before I touch live payments): wire the $99 purchase→provision→magic handoff to land buyers in pp-cloud (currently Mac/Fly provisioner). NOT done — flagged.

--- 2026-06-20 ~21:45 ---
PP PAYMENT CUTOVER → pp-cloud canonical (DONE):
- Worker Stripe webhook created we_1TkMfQS3wE7niBWKot1LVvfo → pp-cloud.weddingsimplified.com.au/webhook/stripe (checkout.session.completed). STRIPE_WEBHOOK_SECRET set on worker; endpoint live + enforces sig (400 no-sig). Worker provisions PP into D1 + magic link to pp-cloud (PUBLIC_BASE_URL).
- Fly PP webhook we_1TiCIXS3wE7niBWK7EtroZHC DISABLED (stops old Fly provisioning). Mac delivery-server ALREADY PP-no-op since 2026-06-14 (only handles $27/$67 WS bundles — untouched, safe).
- Net: ANY PP purchase now provisions ONLY on the worker → pp-cloud. Real $99 buyers fire via amount==9900; metadata.product=pocketplanner covers $0 (promo) case.
- PP_CHECKOUT_URL fixed → buy.stripe.com/6oUfZj43f5rhh2X2iQdIA0L (the one active PP link w/ metadata + promo enabled; plink_1TiCCOS3wE7niBWKzlKXDq5n). Old 28E3… was stale.
- 100% test promo: coupon gmU9r955, code FOUNDER100 (5 redemptions). Daniel tests via the direct link + FOUNDER100 → $0 → pp-cloud magic link → set password → one-click.
- Sales page CTA still → pocketplanner.*/checkout (Fly, 200, JS). Real buyers provision into pp-cloud fine now. Aligning the public button to canonical link = optional follow-up (don't break the live page).

--- 2026-06-20 ~22:30 ---
WS INSTAGRAM — OFFICIAL GRAPH API LIVE (replaces suspended instagrapi):
- Daniel created Meta Business app "WeddingSimplified Publisher" (App ID 896724563444462, secret in .env), added to WS business portfolio, added @weddingsimplified6 as IG tester, generated token via Instagram Login flow.
- Token type = Instagram-login (IGAA…, graph.instagram.com), already long-lived; refreshed to 60d + saved IG_GRAPH_TOKEN. IG_BUSINESS_ACCOUNT_ID=28067553442847885.
- VERIFIED: /me ok (@weddingsimplified6 BUSINESS 155 posts), content_publishing_limit ok (0/25). NO App Review needed (own-account publishing works in dev mode).
- Rewrote agents/ig_graph_publish.py for graph.instagram.com flow (post_image 2-step container→publish, --refresh/--whoami/--limit). Auto-refresh launchd com.ws.ig-token-refresh installed (weekly Sun 03:17) so token never expires.
- instagrapi STAYS disabled (WS_IG_ENABLED=False in sarah.py). Next: wire scheduled posting via Graph API mirroring Pinterest content (needs graphics at public URLs — PN set already on ws-ig-pack.pages.dev). Pending: 1st real test post (live account → needs Daniel OK).

--- 2026-06-20 ~22:50 ---
WS IG AUTO-POSTING LIVE (official Graph API):
- FIRST REAL POST fired ✓ media_id 18088201238103364 (PN-04 plum+olive) to @weddingsimplified6 via graph.instagram.com. End-to-end confirmed.
- Scheduler built: agents/ig_post_runner.py posts 1/day from agents/ig_autopost_queue.json (the 9 IG-pack posts = Pinterest-mirrored PN-01..09 + $99 captions). Images served from ws-ig-pack.pages.dev. State agents/.ig_autopost_state.json (1/day guard + evergreen cycle). launchd com.ws.ig-autopost daily 11:00.
- Seeded state: PN-04=today (won't double-post). Next: PN-02 (12-month timeline) tomorrow 11:00. Cycles all 9 then repeats.
- Token auto-refresh: com.ws.ig-token-refresh weekly. instagrapi STILL disabled (never re-enable).
- TODO later: expand queue beyond 9 (host full social-graphics set publicly) so it's not a 9-day repeat; optional Kip notify per post.

--- 2026-06-20 OVERNIGHT PUSH (Daniel→bed, "do not stop until done") ---
STALE-READ FAILURES OWNED + STRUCTURAL FIX:
- Twice reported from wrong/stale sources (vendor count from PocketPlanner D1 not vendors.db; portal "preview" from .partner-build-staging not the live deploy). Installed ANTI-STALE-READ rule in CLAUDE.md + reinforced feedback_stale_data_structural_fix memory. Rule: verify live DB/URL before any status claim; distrust staging/preview/backup folders.

VENDOR OUTREACH (the real one = WeddingSimplified-Delivery-v2/vendors.db, sender=vendor-outreach-sender.py, launchd com.ws.vendor-outreach 10:30):
- WAS STALLED since 2026-06-17: every send died "Unable to find the server at oauth2.googleapis.com" = Mac wake/DNS race (asleep at 10:30, launchd fires before DNS ready) + a bug that break'd the whole run on the first failure.
- FIXED: added wait_for_network() (retry DNS up to 2min) + changed break→continue with 5-consecutive-fail abort + fails reset. Compiles.
- caffeinate keep-awake installed: launchd com.agency.caffeinate (caffeinate -dimsu, KeepAlive) — Mac stays awake (PreventUserIdleSystemSleep=1 confirmed). Daniel can't set via GUI.
- Ran live catch-up: SENT 80 this run. Totals now: 117 emailed, 8 replies, 4 partners, 1 declined, 13.1% reply rate / 6.6% partner rate. ~2000 still queued (will flow daily now).

PARTNER PORTAL (was wrongly called "not built"): LIVE + functional — weddingsimplified.com.au/partner/login → tunnel → delivery-server (com.weddingsimplified.delivery), HMAC magic-link, dashboard renders real data (verified Paul Osta 11KB). Added login tracking: partner_logins table + _record_login() in verify_token (partner_dashboard.py); restarted delivery-server; verified /partner/login 200 after. NOTE: no historical logins (tracking starts now).

NURTURE: 4 of the 5 'interested' already converted to partners; Styled By KC (info@styledbykc.com.au) = interested-not-signed → set partner_status='nurture'.

WS SITE: mobile header tidied (CSS @media — logo/Blog/CTA were colliding) + blog $27→$99 aligned (1 ref in wedding-budget-template post). Deployed weddingsimplified.pages.dev. CTAs all correctly $99.

IG CONTENT: expanded 9→155 unique posts (built from all Pinterest pins, $99 captions); full graphics hosted at ws-graphics.pages.dev (155 png); shortage early-warning to Kip at 21/14/7 remaining + on full cycle. ig_post_runner daily 11:00. ~5 months runway.

HEALTH CHECKS (sub-agents): ESC backend GREEN (only defect: 3 secondary domains .com.au/.au/.app not redirecting; rest = cutover+sign-off). MS healthy, ingestion ready for Karen tomorrow (/admin Knowledge tab), gated on lawyer sign-off + corpus distillation.

STATUS: WorkSafe VIC API = pending (roadmap doc, no key). Education DB = built (2525 final outreach / 1397 mailable). BPM voice re-render = FLAGGED, awaiting content sign-off (not actioned).

--- 2026-06-21 ~12:15 ---
DASHBOARD brought up to date (Daniel reviews daily, hates .md):
- Replaced stale "REFOCUSED 2026-05-12" priority order with 21/6 order: #1 ESC go-live today · #2 WS live+traffic (+Pinterest $50-100 test) · #3 ESC mailout (Workspace warmed ramp) · #4 BPM · #5 Corvus.
- Added LIVE WS readout (fetches /ws-stats.json): emailed 179 · replies 11 · partners 5 · reply 6.1% · PP sales 1 paid (+1 comp) · queued 2,008. Generator agents/ws-stats-gen.py (vendors.db + Stripe) → launchd com.ws.stats every 30min; new /ws-stats.json route added to dashboard-server.py (restarted). Verified tiles populate.
- Added ESC course sign-off status (1A content✅ audio re-render needed · 1B confirm · Tier-2 drafts). Refreshed Shipped (20-21 Jun) + Awaiting-Daniel sections.
DECISIONS captured: Pinterest $50-100 ad test GREENLIT (reinvest sales). Mailout = Workspace domain warmed ramp (NOT MailerLite — cold list violates ToS + firewall risk); MailerLite for warm nurture later.
ESC CONTENT AUDIT (real, from files): 1A 6 modules content done (12 Jun) but ALL audio predates rewrite → needs BPV re-render (03+05 lack v3). 1B 5 modules content+audio aligned (2 Jun) but NOT updated by Daniel's 12-Jun dump → confirm. Tier-2 student = 5 DRAFTS only (.md, ~29.5k words) → needs decisions+build+narration. Top-level modules/ = legacy/parallel set.

--- SESSION CLOSE 2026-06-21 14:19 [auto — idle 2h] ---
Session ended without manual sign-off. Check archives for last active snapshot.
Next session: review session-log.md and pick up from last in-progress items.

--- 2026-06-21 ---
Built RAG grounding for Marriage Simplified "Karen" AI (D1 + CF Workers AI embeddings, NO Vectorize per token perms). In marriage-simplified/:
- schema.sql: added kb_chunks(id, program, source, title, chunk_text, embedding TEXT[768-float JSON]) + idx on program/source.
- scripts/ingest-kb.mjs: resumable Node ingester. Maps each text-full/*.txt via manifest-full.tsv to program+title, chunks ~250w/40 overlap, embeds via CF REST @cf/baai/bge-base-en-v1.5 (batched 50, retry+backoff), byte-budget multi-row D1 inserts (60KB cap — fixed SQLITE_TOOBIG from large embeddings). Processed-list .kb-ingest-processed.json makes it resumable.
- src/chat.ts: retrieveKnowledge() before model call — embeds query via env.AI binding, narrows 1-3 programs via keyword map (PROGRAM_KEYWORDS), loads only those programs' chunks from D1, cosine top-6, injects "RETRIEVED FROM KAREN'S OWN KNOWLEDGE" block. Falls back to all-programs if no keyword match. Never blocks chat on RAG failure.
- src/prompt.ts: accuracy gate reinforced — retrieved passages = authoritative source of truth for specifics.
- wrangler.toml already had AI + DB bindings (confirmed via dry-run bundle).
Dry-run PROVEN: 20 docs → 109 chunks inserted to remote D1, retrieval test returned sensible top-6. Proof data then CLEARED; D1 kb_chunks empty, processed-list reset. NOT deployed, full ~10k batch NOT run — Daniel kicks that off after review.
Full run cmd: export CF creds; node scripts/ingest-kb.mjs (also needs full schema applied: kb_chunks table created remotely already).

--- 2026-06-21 (cont) — Karen disclosure reframe + ESC Module 4 ---
KAREN (Marriage Simplified): Daniel reframed the AI disclosure — credit the REAL Karen Gosling's 40 years (Gold Coast counsellor), funnel "more support" → her for paid F2F (NOT generic "a professional"), keep AI disclosure + crisis lines. Updated brand.ts (new SAFETY_FOOTER + DISCLAIMER_FULL single source), landing.ts, frontend.ts (please-read + consent + 35→40yr), prompt.ts (WHO-YOU-ARE + FACE-TO-FACE→real Karen). tsc clean. Deployed worker (version 554e7110). Note: PUBLIC_BASE_URL still workers.dev — repoint to marriagesimplified.com.au next.

ESC MODULE 4 (04-compliance-stack.html) — Daniel's review notes applied + DEPLOYED LIVE (easystagecraft.com/course/modules/tier-1a/04-compliance-stack, 200):
- WHS/OHS records → log in EasyRisk, export/print to hand school WHS officer (EasyRisk IS the tool); equipment register → EasyInventory.
- "Show me the ticket" → new section: manage all people+licences in EasyRisk with expiry flags + SWMS assignment gate (+screenshots noted for resource pack).
- Insurance → contractor PL+WorkCover certs a CONDITION of accepting quote; + EasyRisk contractor portal (rep uploads certs + crew → crew flow to EasyScheduler/show lists).
- WWCC → mandatory gate on every AU person added in EasyRisk; missing/expired blocks assignment.
- Audit → EasyRisk Save/Archive Show → dated bundle, Archive shows storage age, 7-yr "retention reached — delete?" prompt.
- Suite tie-in expanded; Exercises 4.1 (build pack hands-on IN EasyRisk+EasyInventory) + 4.2 (drafting) wired with tool CTAs.
- Knowledge check → saved reflection textareas (esc_m4_reflections, correct inherit font).
- Resources → single M3-style table (real M04 links + descriptions, EasyRisk-native vs extras); removed duplicate auto-injected section.

PRODUCT FEATURES from Module 4 captured in EASYRISK-PRO-SCHOOLS-SPEC.md (People&Licences board, WWCC gate, Contractor portal→Scheduler, Audit/Archive 7yr, EasyInventory test-and-tag) + retention/anti-churn strategy. Launched bg agent to extend /risk/schools/ prototype with the 4 ER features for UX review (not deployed by agent).

Schools prototype v1 shell deployed to app.easystagecraft.com/risk/schools/ for UX review (live /risk/ untouched).

--- 2026-06-21 (cont) — ESC Modules 5 & 6, Module 1 ref, EI + EasyRisk builds ---
MODULE 5 (budget) DEPLOYED LIVE: ROOT-CAUSE fix — `.callout strong{display:block;uppercase;13px}` was hitting inline figure <strong>s inside worked examples (the "different font + split line breaks" Daniel saw). Scoped to `.callout > strong:first-child` + `.callout p/li strong{color:inherit}` — swept across ALL 6 tier-1a modules (01,02,03,04,06 patched; 05 native). Also: parent-comms cadence aligned to 24wk timeline (−12→+2, en-dash, even beat); post-mortem rewritten blunt per Daniel ("not feel-good, table the painful stuff, same team next year, not about feelings"); exercises 5.1/5.2 wired to EasyScheduler/EasyInventory; kc → saved reflection fields (esc_m5); resources → table; auto-injected dup removed.

MODULE 6 (capstone) DEPLOYED LIVE: SOP spelled out (standard operating procedure); app links added to all 6 deliverables; SUBMISSION redesigned as AUTONOMOUS "Capstone Scanner" (reads app data + few uploads, click Submit for scan, instant per-deliverable result, certificate auto-issues — no in-person, no human assessor); added "How you pass" (all 6 Pass criteria + ≥70% quizzes); start-here app links; resources → table; auto-injected dup removed. Scanner BACKEND is a build item (designed in course, not yet wired).

MODULE 1 (03-teacher-as-pm) DEPLOYED: added "What 1A is — and what comes after it" callout (1A=frameworks/unbullshittable on process, 1B=vocabulary/unbullshittable on kit) per Daniel.

EASYRISK: Daniel reviewed /risk/schools/ prototype — "yes yes yes, make this the standard for ER too." Spec updated. Launched bg agent: build /risk/pro/ commercial dashboard (no principal/school language) mirroring schools + Pro⇄Schools toggle in toolbar of all 3 pages (localStorage easyrisk_mode). Not deployed by agent.

EASYINVENTORY: Daniel feature req — bg agent building: (assets) test&tag date + service date + auto +12mo next-due + "upcoming test & tags" view; (costumes) size field + multiples per item (item.sizes=[{size,qty}], total qty=sum). Additive-only to item schema, KV-sync safe. Not deployed by agent.

--- 2026-06-21 (cont) — EI UX fixes + EasyRisk Pro/toggle/tiles DEPLOYED ---
EASYINVENTORY (app.easystagecraft.com/inventory) deployed: (1) Subcategory converted from input+datalist → SELECT with "+ Add new subcategory…" (mirrors Category control) — _populateSubcatSelect/_subcatUniverse/onSubcatSelectChange, sources from _loadSubcatOrder + item subcats; (2) date calendar icon greyed (::-webkit-calendar-picker-indicator filter invert .7); (3) prep-list select+buttons+label restyled to match .search bar (8px/12px, 14px, radius 8px). Fully additive, validated, deployed.

EASYRISK deployed (app-easystagecraft):
- Pro dashboard /risk/pro/ (commercial language, mirrors schools), Pro⇄Schools toggle in toolbar of /risk/, /risk/schools/, /risk/ (localStorage easyrisk_mode).
- Quick-grab tiles row on BOTH pro+schools (above status board): 🚑 Submit incident · ⚠️ Submit near miss · 📋 Compliance report(→#board) · 📒 Register(→#register) · 📚 Libraries. qtScroll() smooth-scroll; unwired ones → protoAct toast.
- RA + SWMS cards upgraded to builder-card with big primary CTA ("▶ Open the Risk Assessment builder" + helper) — obvious click target for teachers. Links unchanged (→ live /risk/ builder).
All UX shells with sample data (prototype banner). Pending: wire the prototypes to real KV data; decide whether /risk/pro/ becomes the /risk/ standard landing.

PENDING BUILD ITEMS: Capstone Scanner backend (autonomous assess); EasyInventory test-and-tag was the M4 source; Karen RAG ingest finish+verify + MS base URL repoint + transcription + GitHub PAT + Mike EI content.

--- 2026-06-21 (cont) — Tier 1B M1-M5 complete + ESC Store concept ---
ESC 1B all deployed: M1 Speak Lighting, M2 Speak Sound, M3 Speak Stage Mgmt, M4 Reading a Lighting Plot (+ real plot image), M5 Reading a Sound Spec. Standard treatment on each: callout-figure CSS fix, kc→saved textareas, resource→table, CPD yellow-at-bottom, tile-capitals, tie-in app links, auto-injected dup removed. Global rename "Advanced Diploma"→"Bachelor of being Unbullshittable" across course.
1B-4 content: movers corrected (8-12 reasonable for 2026, not over-spec; flag touring-grade/excess instead); groundrow softened to "valid unless deliberate look"; tie-in honestly reframed (suite is NOT a plot drafting tool — EasyScheduler focus-hrs + EasyRisk rigging-hazards only).
1B-5 content: NEW "Radio mics + headsets — school reality" (JAG-class not DPA; cast never fit/remove own mics); ESC Store Mic-Up-Kit reference; tie-in = EasyOrchestra orch-layout→input-list→console-class→budget chain.
ESC STORE (saved project_esc_store.md): Matt/Besen idea — consumables storefront, PO+invoice checkout, Mic-Up Kit + tapes + batteries, open to anyone. NOT BUILT — 1B-5 links to /store/ which 404s until built. Build = storefront + PO/invoice checkout (reuse ESC invoice infra).
EofGRAPHICS: Daniel wants graphics to break up module text across all modules — asked for SUGGESTIONS before execution. Pending my proposal.

--- ESC STORE MVP built [2026-06-21] ---
Built easystagecraft.com/store/ — single-page storefront for theatrical consumables with school PO/invoice checkout.
- NEW FILE: easystagecraft/store/index.html (brand-matched to pricing.html/index.html: same CSS vars, header/nav, footer).
- 10 products (Mic-Up Kit, Gaffer Matt/Std, Electrical Tape colour pack, SM Spike colour pack, Glow Tape, Dance Floor Tape, AA/9V batteries, Makeup Hygiene Kit). Indicative AUD prices, clearly labelled "confirm at checkout".
- localStorage cart (key esc_store_cart_v1), cart drawer, qty steppers, live total.
- Checkout captures org/contact/email/phone/PO/address/notes. Payment = "Invoice me (PO)" primary; "Pay by card" stubbed as Coming soon (NOT wired).
- WORKER (additive, 182 insertions, 0 deletions): POST /store/order in agents/auth-worker/cf-magic-link.js. Reuses SESSIONS KV (STOREORDER:{ref}, 400d TTL), Gmail send mechanism (same as feature-request), rate-limited store_order=6/min, server-recomputes total, honeypot. Emails admin notification + buyer confirmation. Returns {ok,orderRef ESC-xxxxxxxx}.
- Page POSTs to https://auth.easystagecraft.com/store/order with X-Requested-With: ESCApp (satisfies CSRF; origin whitelisted).
- NOT DEPLOYED (per instruction). Validated: wrangler dry-run OK; both inline scripts node --check OK; one </body>/one </html>.
- DANIEL DECISIONS PENDING: (a) final prices, (b) fulfilment/supplier, (c) card payment stub. Optional: set STORE_ORDER_TO secret (defaults to FEATURE_REQUEST_TO / support@easystagecraft.com).

--- 2026-06-21 (cont) — 1B-6 Wardrobe&Makeup + ESC Store + Capstone D7 ---
NEW MODULE 1B-6 Wardrobe & Makeup (06-wardrobe-makeup.html) DEPLOYED: make/hire/buy routes, alterations=biggest hidden cost, NEW "Wardrobe labour is the cost centre" section (3-7 dressers side-stage, hair&makeup crew per show, daytime maintenance/wash/repair/turnaround shift), sizing+multiples→EasyInventory, makeup+hygiene, wigs, costume-hire BS-detector (A$8,940→A$5,040), EasyInventory+ESC Store tie-in, kc fields, resource table, yellow CPD, finale "Tier 1B complete" banner with accurate assessment model. House style + validated.
1B MAPPING all updated: counts 5→6 across M1-5 crumbs; dashboard /learn/ T1B array +Wardrobe&Makeup (n:12); 1B-5 de-finalized (next→1B-6, old finale banner removed); CPD totals 9.0 (1B) / 18.0 combined / 12 modules.
ASSESSMENT MODEL (Daniel-approved): 1B = completion+portfolio (NO quizzes — fixed the inaccurate "Tier 1B quizzes" wording). Standalone→scanner checks portfolio→1B cert. Bundle→Capstone DELIVERABLE 7 (read+challenge lighting+sound quotes, 1B in action) → combined "Bachelor of being Unbullshittable". D7 added to capstone deliverables + rubric row + intro. Global rename Advanced Diploma→Bachelor done earlier.
ESC STORE LIVE at easystagecraft.com/store/ (MVP): brand-matched storefront, product grid (Mic-Up Kit + gaffer matt/std + electrical/spike/glow/dancefloor tape + AA/9V batteries + makeup hygiene kit), localStorage cart, PO/invoice checkout → POST auth.easystagecraft.com/store/order (additive worker endpoint, stores in SESSIONS KV STOREORDER:{ref}, emails admin+buyer via Gmail API, rate-limited; deployed+smoke-tested 400-on-empty). Card = "coming soon" stub.
  FLAGS FOR DANIEL: (a) prices INDICATIVE — confirm before promoting; (b) fulfilment/supplier = Daniel decision (orders just captured+emailed); (c) card stubbed.
PENDING: 1B audio pass (doctor fix needed first — fails on long modules); SVG diagram system (approved); 1B-6 resource-pack files (8 slugs, like other packs); EasyRisk P1-P3+Scanner; Karen RAG follow-ups.

--- SESSION CLOSE 2026-06-21 ~late ---
Completed (deployed): ESC 1A M1/3/4/5/6 + 1B M1-M6 (incl NEW 1B-6 Wardrobe&Makeup) + global "Bachelor of being Unbullshittable" rename + Capstone Deliverable 7 + assessment-model fix + dashboard/mapping (12 modules, CPD 18.0); EasyInventory (test&tag + costume sizes + UX fixes); EasyRisk (Pro dashboard + Pro/Schools toggle + tiles + route-wiring + P0 doc storage worker+UI); ESC Store MVP live (PO/invoice); Karen disclosure reframe; 1A audio M1/4/6. Catfish check reported (HIGH fake). Anthropic credits topped up by Daniel (Karen restored).
In progress (bg agent, review+deploy next session): Store Stripe card + featured Mic-Up/Make-Up kit tiles = agent a09ca60518fe2236a (output in tasks/). 
Decisions LOCKED: kit longevity = razor+blades (durable Make-Up kit + per-production refills; Mic-Up ~per-production). 1B audio = de-prioritized ("no worries" — Daniel).
Supplier search DONE: hybrid model (China branded box + assemble in AU); low-MOQ box = Shenzhen Yincai (MOQ 2). ⚠️ REGULATORY: condoms=ARTG/sponsor (redesign to non-therapeutic cover OR buy ARTG wholesale in AU); cosmetics=AICIS+ACCC labelling. See project_esc_store.md.
Blocked on Daniel: store final prices, fulfilment, condom-vs-alt-cover decision, AICIS/cosmetics compliance, product photos.
Next session: (1) review+deploy store Stripe/kits agent output; (2) re-rank suppliers low/no-MOQ (Yincai lead); (3) SVG diagram system (approved); (4) fix script-doctor hang then 1B audio when Daniel wants; (5) EasyRisk P1-P3 + Capstone Scanner; (6) Karen RAG/base-URL/transcription/GitHub PAT.
Known issue: script-spoken-doctor.py / anthropic_call hangs on API call (no read timeout) — pkill to clear; fix before relying.

--- ESC STORE ENHANCE [2026-06-21] ---
Two changes to live ESC Store (NOT deployed — dry-run only):
1. Storefront restructured: KITS[] (Mic-Up Kit $189, Make-Up Kit $64) now FEATURED in own #kits section with accent-bordered .kitCard tiles, "★ EasyStagecraft original" badge + "What's inside" list. CONSUMABLES[] (gaffer/tape/glow/dance/batteries) in separate "Show consumables" grid. PRODUCTS = [...KITS,...CONSUMABLES] keeps cart sku-driven. Renamed MAKEUP-HYG → MAKEUP-KIT.
2. Stripe card checkout un-stubbed. Worker: additive POST /store/checkout (rate-limit 'store_checkout' 6/min, CORS+CSRF same as /store/order). Rebuilds line_items from server-side STORE_PRICE_MAP (source of truth, never trusts client prices) via stripeForm + STRIPE_SECRET_KEY (mirrors handleEscCreateSession). mode=payment, AUD price_data, customer_email, success_url=.../store/?paid=1&ref={CHECKOUT_SESSION_ID}, cancel_url=...?cancelled=1. Returns {url}; storefront redirects. ?paid=1 clears cart + shows success. Card is now default option; PO fields conditionally required (poOnly class).
Worker diff: 107 insertions, 0 deletions (strictly additive). wrangler dry-run OK (215KiB upload, pk_live_ bound). Storefront: 2 inline scripts both node --check OK, 1 </body>/1 </html>.
FLAG: prices still indicative — need Daniel's confirmation. STRIPE_SECRET_KEY must be live ESC key (pk_live_ publishable confirms live mode). NOT deployed per instruction.

--- SESSION OPEN 2026-06-21 19:05 ---
Built Karen Gosling / MarriageSimplified.com.au transcription pipeline (URGENT task).
- Source: rclone remote odkaren:MarriageSimplified.com.au (READ-ONLY, touched nothing else). 608 A/V files, 55.57 GB.
- Engine DECISION: CF Workers AI Whisper free tier was EXHAUSTED on first call ("used up your daily free allocation of 10,000 neurons"). Auth/token were fine. Pivoted to LOCAL whisper.cpp (whisper-cli + ggml-base.en.bin already at data/whisper-models/) — no quota, no size limit, fully offline & robust. CF path kept as USE_CF=1 fallback (chunks to 45s segs).
- Script: marriage-simplified/scripts/transcribe.py — resumable (manifest.json), per-file try/except, idempotent skip-done, logs to transcribe.log.
- Outputs: karen-corpus/transcripts/*.txt, /html/*.html, /pdf/*.pdf (Chrome headless HTML->PDF — pandoc had no LaTeX engine), corpus.txt appended for RAG.
- Running detached: nohup caffeinate -i python3 -u transcribe.py & (PID in scripts/transcribe.pid). Verified resume skips done, 2 files done w/ txt+PDF, on file 3/608.
- NOTE: ~5-6 min/file on CPU => full run is long (days). Runs unattended; survives session. Optional speedups: switch model to ggml-small.en, or flip USE_CF=1 once CF neurons reset (daily).

--- ESC PVC NARRATION: Tier 1B (all 6) + 2 Tier 1A stragglers [2026-06-21 ~21:05] ---
Brought 6 Tier-1B sidecars in line with current deployed HTML, doctored all 7 sidecars, rendered PVC audio, deployed.
- Sidecars updated to match HTML: 01 (DMX river-of-info, bar downstage→upstage, trim flies-out, "bullshit you", swap-a-mover), 02 (sound-op 5hr show call, radio-mics venue-first section), 03 (post-rehearsal admin paid + EasySM home), 04 (movers 8-12 reasonable 2026, groundrow deliberate-look, honest suite tie-in NOT a plot drafting tool), 05 (radio-mic/headset school reality JAG-not-DPA + cast-never-fit-own-mics + Mic-Up Kit; EasyOrchestra orchestration→input→console→budget chain), 06 (NEW full sidecar written from scratch).
  NOTE: the JAG/DPA/Mic-Up-Kit content the brief attributed to module 02 is actually in module 05's HTML — put it in 05.
- Doctor: 01-05(1B) passed first try via non-streaming. 06, 05-budget, 03-teacher hit sustained Anthropic read-timeouts (120s then 360s) on large 12k-token non-streaming calls during an API congestion window (also saw one HTTP 529). Fixed by switching to an SSE STREAMING doctor (/tmp/doctor_stream.py) which kept the socket fed — all 3 then passed first try. Small Anthropic calls were fine throughout (2.3s); only large non-streaming completions failed. Lesson: spoken-doctor should stream for big modules.
- Rendered all 8 via esc-course-narrator --force --stability 0.30 --style 0.35. 1B 01-05 -> .audio-v3.mp3 (matches HTML); 1B 06 + 1A 05-budget + 1A 03-teacher -> plain .audio.mp3 (matches HTML). Injected a new audio player into 06 (was placeholder comment) and 03-teacher (had no player + no audio CSS — added both) matching the other modules' .audio-player-injected markup.
- Deployed easystagecraft via wrangler (19 files). Verified live 200s: all checked mp3 byte-sizes match local renders; 06 + 03-teacher players live.

--- ESC front-end pre-onboarding pass [2026-06-21] ---
Final ESC teacher-facing front-end pass before onboarding emails.
LINK CHECK: all internal links across course index, dashboard, APST, curriculum, account, pricing + all 12 module pages + pay SKUs + invoice links = 200 live. Zero broken links.
APST: apst-mapping.html + curriculum-mapping.html already existed (branded). Surfaced further — account.html now has dedicated "APST Mapping · CPD hours" tile (was only curriculum tile); sales page hero pill now "18 CPD hours · APST-mapped · 12 modules" + added "See the full APST mapping" link in modules sub. Dashboard already linked both.
FIXED STALE COUNTS (canonical = live Wardrobe module + task brief: 12 modules, 1A=9.0h + 1B=9.0h = 18.0h, 1.5/module, teacher-as-PM=1.0, capstone=2.0):
 - course/index.html: 11->12 modules, 1B 5->6 modules, added Wardrobe (1B6) module card, 28->18 CPD hours, tier headings 18h->9h / 10h->9h, bundle 11->12 modules, rewrote stale "70% built / 5 of 6 modules" FAQ to "all 12 live".
 - apst-mapping.html: stat strip 1A 18->9h, 1B 10->9h, bundle 28->18h, added M12 Wardrobe card, tier totals fixed, Tier2 40->30h, cert footer 28->18h / 40->30h, state-fit lines corrected (each tier 9h, bundle 18h vs 20h/yr states), VIC 140%->90%.
 - learn/index.html: cert body 18h/28h -> 9h/18h, locked-cert 12h->9h.
 - curriculum-mapping.html: legend clarified that legacy topic taxonomy maps to current 12-module 1A/1B shape (deep table rework FLAGGED for Daniel — uses old 10-module student-curriculum numbering).
DEPLOYED: easystagecraft (89c39c7d) + app-easystagecraft (6d709adc). All verified 200 + content live.
FLAG FOR DANIEL: curriculum-mapping.html tables still use legacy 10-module numbering (1-10) vs current 1A/1B 12-module structure — needs a proper rework pass, not done here (risk of inaccurate curriculum claims if rushed).

--- TIER 2 STUDENT COURSE — FINALISED & DEPLOYED [2026-06-22] ---
Built the LIVE Tier-2 student course (text lessons) from the RESOLVED markdown drafts
(course/modules/tier-2-drafts/*.md — Daniel's 2026-06-21 resolution pass) + RESOLUTION-DECISIONS.md.

Key finding: the existing course/tier-2-review/ HTML was STALE (built 12:41 same day, BEFORE the
resolution pass) — it carried 5-7 amber REVIEW flags/page and full "DRAFT / not live / DO NOT DEPLOY"
chrome. NOT suitable to ship. Rebuilt fresh.

NEW live location: easystagecraft/course/modules/tier-2/ (sits beside tier-1a/tier-1b).
  - index.html (student landing, 5 subjects + glossary), glossary.html (85 terms, carried from review),
    + 5 subjects: stage-management, lighting-basic (=Lighting Fundamentals), lighting-design,
    sound-basic (=Sound Fundamentals), sound-design. ~29k words.
  - Live course styling (blue accent, account-badge), NO draft banner. Build script: _build-live.py.
  - Remaining open items rendered as honest neutral Phase-2 notes (NOT amber flags): SM show-call
    recording "coming in a future update"; lighting-basic fixture reference photos "coming soon";
    lighting-design VCE/SACE assessment caveat + "ellipsoidal/ERS" term-confirm. All [image to confirm]
    author slots rewritten to clean "Reference photo coming soon: <model>" captions.
  - Fact-fixes verified present: Aviom (not Avium), 20Hz-20kHz, George (not Charles), Fresnel
    concentric-ring (not concave), Conrad refs cut from sound-design, carry (not curry) rehearsals.

Wiring:
  - functions/course/_middleware.js: added /course/modules/tier-2/* → requires tier_2 (mirrors
    tier-1a/1b pattern; School-Licence gate). NOTE: middleware paywall currently returns 200 for
    tier-1a too (not enforcing for anyone right now) — that's the plumbing agent's domain, untouched.
  - course/index.html: Tier 2 bullet flipped from "57 video lessons · Coming soon Phase 2" to
    "written lessons available now ✓ Live · narrated video Phase 2".
  - course/learn/ dashboard: added Tier 2 student-course card linking ../modules/tier-2/.

Deployed via wrangler pages deploy easystagecraft (branch main). All pages 200 live:
  easystagecraft.com/course/modules/tier-2/ + /stage-management + /glossary (85 dt) etc.
  HTML validated (1 html/head/body each, html.parser clean, 0 leftover [REVIEW/draft chrome).

Next (Daniel/Phase 2): supply fixture reference photos; confirm "ellipsoidal/ERS" term + current
VCE/SACE assessment detail; drop in show-call comms recording; record narrated video versions.
The stale course/tier-2-review/ dir can be archived/deleted (superseded) — left in place for now.

--- PAYWALL FIX + STAGED DEPLOYS [2026-06-22] ---
🟥→✅ COURSE PAYWALL FIXED. Root cause: `wrangler pages deploy easystagecraft` ran from /Users/kip/agency, so wrangler looked for ./functions (nonexistent) and swept easystagecraft/functions/ in as static assets — Functions never compiled, every gated path returned 200 raw.
FIX: deploy from INSIDE the dir → `cd easystagecraft && wrangler pages deploy . --project-name=easystagecraft`. Output now shows "✨ Compiled Worker successfully / Uploading Functions bundle".
VERIFIED gating (all 302 unauth): tier-1a/1b/2 lessons, dashboard, resource-packs M0x + tier-1b, venue-vault, legacy quiz paths. Public (landing/sales/curriculum) still 200/308. FAIL-CLOSED confirmed: bogus esc_session cookie → 302 (does not fail-open). Auth worker /entitlements → 401 (alive). Redirect target = /course/?need=<tier>&next=<path>#pricing.
NOTE: middleware source was never actually leaking — /course/_middleware.js 200 is the SPA fallback HTML, not source. My earlier "served as static" wording was imprecise; the real defect was non-compilation.
REMAINING (Daniel, before emailing teachers to buy): one real end-to-end login + open-a-lesson test to confirm a PAYING user gets 200 (can't mint a session here without a magic-link email). Lock side fully proven; buyer side is logically sound (same /entitlements call the dashboard uses) but untested live.

✅ AUTH WORKER deployed (agency-auth, version b28e8162) — additive staged changes now LIVE + smoke-tested (all 401/403 guarded, none 404/500):
  - Capstone Scanner: POST /capstone/scan, GET /capstone/status
  - Tier 2 plumbing: /tier2/seats /tier2/seats/invite|revoke /tier2/progress /tier2/cross-suite-seat, /course/progress GET+POST
  - Store Stripe: POST /store/checkout
✅ TIER 2 ADMIN dashboard deployed → app.easystagecraft.com/tier2-admin/ (200; client-side entitlement-gated via worker).

Still running: Karen transcription (looping, restored OneDrive path), text corpus ingested, Mac caffeinated.

--- CAPSTONE GRADER + QUIZ GATE + SCHEDULER SEAT [2026-06-22] ---
Cross-suite seat: added EasyScheduler ('es') to the switchable set + made it the DEFAULT (it's the app the 1A capstone is built in) — was EasyRisk. Worker handleTier2CrossSuiteSeat allowlist + activateTier2CrossSuiteSeat default; tier2-admin dashboard button/label/copy. Deployed (worker 76fde183, app live).
FLAGGED to Daniel (undecided): tier_2 already grants owner ALL apps, so "assign cross-suite seat to yourself" is a no-op for the buyer — real value is handing one app to a co-teacher. Suggested reframing the feature copy as "give a colleague a seat"; NOT touching the locked tier_2=everything decision.

Capstone Scanner upgraded (worker 2ef20d6d), Daniel-approved this session:
- LENIENT AI grader (Haiku) on the writing pieces (D3/D5-brief/D6/D7). Rule: pass if relevant + genuine attempt even if imperfect/misplaced; fail only off-topic/empty/no-attempt; constructive feedback either way. Fail-OPEN on API error. Binary formats accepted on presence (only textual uploads graded). Concurrent grading.
- QUIZ gate: Modules 1-5 quizzes now required (checkQuizzes reads COURSE_PROGRESS server-side; recorded score == passed; lockout-safe). Folded into corePass.
- Copy fix on 06-capstone.html: "assessor review" → autonomous Capstone Scanner / AI assessor / re-scan freely.
All additive, dry-run clean, deployed; /capstone/scan + /status smoke-tested (403/401 guarded).

--- ESC PRICING RE-MAP — FULL PLUMB-IN [2026-06-23] ---
Daniel confirmed full model (12-mo timebox, Production Suite $1090/1990/3490, EasyStageManager name). Plumbed end-to-end, DEPLOYED (worker 2cbef4d8):
WORKER (cf-magic-link.js): ALL_TIERS +stage. addEntitlements gains expiresAt (per-key expiry map); readEntitlements filters expired. entitlementsFromStripeTier: 1A/1B +es; bundle +stage; SUITE→apps-only (was ALL_TIERS) — kills the Tier2/Suite arbitrage. timeboxedKeys() + COURSE_APP_TIMEBOX_DAYS=365: course one-off app keys 12-mo boxed (course tier perpetual); both grant sites (card+invoice) split perpetual/expiring. appKeysFromNickname: suite→apps-only, +stage branch. entitlementsForInvoiceTier suite +stage,rost. Comps (besen x2, sarah.butler) +stage so es-coupled SM access survives decoupling. INVOICE_CATALOG: Production Suite new prices + EasyStageManager + ES/ER annual. SUB_PRODUCT_LABELS +new. NEW endpoint POST /roster/subscribe (auth-gated qty-aware subscription Checkout Session, $5/$9).
FRONTEND: easysm REQUIRED_ENT es→stage. easystage got a self-contained 30-min overlay gate on 'stage' (was ungated). roster: add-person + importCrew behind gatePaid; es-holders preview unlimited (ES_PREVIEW) but pay to add personnel; subscribe is now IN-APP (rosterBuySeats → /roster/subscribe, headcount defaults to staff count) NOT external link (Daniel's call). roster UX: main max-width:none (full-width, calendar expands with browser); Personnel converted from modal → in-tab view (setView('personnel')).
STRIPE (script stripe-pricing-2026-06-23.py, idempotent): CREATED EasyStageManager product + 8 prices (stage_*_m/y) + 4 annual payment links. Production Suite: renamed 3 Suite products, new annual prices ($1090/1990/3490) + links, deactivated old. EasyRoster $5/$9 per-seat prices already existed → adjustable-qty payment links created + in-app endpoint. Links saved: marketing/stripe-links-2026-06-23.json.
CANONICAL-pricing-access.md reconciled (new access matrix incl Stage/Roster cols, 12-mo footnotes, Production Suite, in-app roster).
SMOKE: /roster/subscribe 403 (guarded), all apps 200, payment links 200, worker dry-run clean.
REMAINING (flagged to Daniel): EI/ES/ER uniform-grid reprice (2026-06-16 approved) NOT rolled to live prices/sales pages — own focused pass. Buyer-side paywall e2e test still pending before teacher emails.

--- OVERNIGHT BUILD [2026-06-23, Daniel asleep] ---
PRICING ROLLOUT (2026-06-16 grid) — LIVE: EI/ES/ER repriced to uniform grid in Stripe (EI/ES $9/29/49/99; ER premium $19/49/99/199), seats 1/5/15/40; old prices deactivated; nickname collisions (ei_school_y/es_school_y) cleaned. Scripts: stripe-grid-reprice-2026-06-23.py. EO left (amounts already uniform). All payment links (m+y) in marketing/PAYMENT-LINKS-MASTER-2026-06-23.json.
SALES PAGES (7 parallel agents + review): EO/EI/ER/ES pages → new grid + links; EasyStageManager sales page CREATED at /easystagemanager/ (StageWrite-alternative, SM+Stage unified); old /easysm /easystage got banners; pricing.html + homepage + course/index freshened (Production Suite not Whole-Suite, EasyRoster, EasyStageManager). Leftover "in build"/"Coming soon" cleaned. Deployed.
SCREENSHOTS: Playwright (venv /tmp/pwshot) captured all 7 apps with demo data; scheduler + roster excellent, risk needed disclaimer-bypass (er_ack_accepted_v1). Embedded on every app sales page. assets/screenshots/.
GATE AUDIT (all apps): all have 30-min timer + wall + paid-gating EXCEPT found+FIXED EasyInventory exportPicklistCSV() was ungated → now requirePaidAction. 
CANONICAL-pricing-access.md fully reconciled (grid live, footnotes updated).
DASHBOARDS (Daniel's bedtime ask): 
 - dashboard-server.py: NEW endpoints /api/ws/vendors (get_ws_vendors — reads LIVE WeddingSimplified-Delivery-v2/vendors.db: 3039 vendors, 259 emailed, 22 replies, 13 partners; replies/enquiries/partners/outreach) + /api/esc/clients (get_esc_clients — Stripe subs+course purchases → CRM: 4 clients, $172.50 MRR, programs/status/since/MRR + sales + agency costs). Restarted via launchctl kickstart com.agency.dashboard.
 - ws.html: Vendor CRM section (tabs: replies/enquiries/partners/outreach) — the "vendor login drill-down" ask.
 - esc.html: Clients·CRM section + freshened access matrix (Stage/Roster cols, Production Suite) + pricing table (full live grid). Sales+costs in CRM stats.
 - index.html (master): added EasyStageManager + EasyRoster tool-cards; fixed stale course desc (12 modules/1A+1B+Tier2).
ALL verified: dashboards 200, worker 401, paywall 302, payment links 200, caffeinate active.
STILL RUNNING: Karen transcription 116/611 done (494 pending, daemon active) — RAG ingest waits for completion.
REMAINING / for Daniel: (1) buyer-side paywall test (his to do). (2) broad subjective freshen of bpm/corvus/finance/master dashboards — left for his eye. (3) course-progress-per-student in ESC CRM needs an auth-worker admin list endpoint (flagged TODO). (4) EI/ES/ER are now LIVE-SELLABLE at new grid — state change to be aware of.

--- COURSE-PROGRESS-PER-STUDENT WORKER ENDPOINT [2026-06-23] ---
Built the admin endpoint Daniel asked for so the ESC CRM shows where each learner is (support visibility).
WORKER (agency-auth, version a69e798c): GET /admin/course-progress — auth via X-Dash-Key header (server-to-server) OR admin browser session (requireAdmin). Lists COURSE_PROGRESS:* via adminListKeys, returns per-student {email, modules_done, tier_1a x/6, tier_1b x/6, last_module, last_at, quizzes_passed, capstone (certified/passed/scan attempted/—), certificate, updated}. New secret DASH_ADMIN_KEY set via wrangler secret put + stored in agents/.env. Tested: no-key→403, with-key→200.
DASHBOARD: get_esc_clients() now calls /admin/course-progress with the key + merges progress into client rows by email; esc.html CRM table gained a "Course progress" column. Stale "needs endpoint (TODO)" note removed.
Currently shows "—"/count 0 because no real course students have server-side progress yet — the front-end (course/progress-sync.js + learn/index.html) POSTs to /course/progress on module-complete, so the CRM populates live as students learn. All verified (dashboards 200, endpoint guarded).

--- DASHBOARDS + STATUS AUDITS + IMAGE-GEN AUDIT [2026-06-23, Daniel at work] ---
DASHBOARDS (all done, verified 200):
- ws.html + esc.html: "🔥 Action items" panels at top (road/mobile, mailto tap-actions). WS: interested-vendor onboarding list + waiting brides + pending outreach. ESC: payment-issue clients + support-needed students.
- index.html (master): added EasyStagecraft LIVE READOUT (clients/MRR/active-subs/course-sales/need-action via /api/esc/clients + loadEscStats) alongside the existing WS readout.
- finance.html: grand-revenue now wired (ESC MRR + course sales, income vs burn).
- corvus.html + bpm.html: LIVE-VERIFIED truth banners at top (the existing widgets read stale state).

🔴 THREE BIG FINDINGS (all stale-data failures — things believed running that aren't):
1. CORVUS STALLED: Venus agent DEAD since 2026-06-03 (~20d), NOT loaded in launchd (no plist). 0 subs/$0. Mia+Voss fully built. Gen stopped ~47d. Sessions expired. Health-check "5/5" reads a frozen June-3 file. (nsfw/CORVUS-STATUS-2026-06-23.md)
2. BPM UNSHIPPED: 0 videos ever published, 0 subs. Render stack works (2 topics rendered 05-06), stopped ~05-25. com.agency.alex DOESN'T EXIST in launchd. ASIC approved (monetisation unblocked). YouTube OAuth expired 05-12. (BPM-STATUS-2026-06-23.md)
3. IMAGE-GEN WIRING GAP: v3 pipeline (PuLID@0.7+DWPose+ADetailer+UltraSharp on JuggernautXL) built+validated+approved, but the nightly agent (overnight-generate.py) still fires OLD A1111 + contaminated v1 LoRAs; v3 only runs by hand. Nightly job currently disabled anyway. HandRefiner built-but-off; NSFW activator LoRA slot empty. Competitors (Higgsfield/OpenArt/ZenCreator/Ryla) resell our exact stack behind a UI. (nsfw/IMAGE-GEN-STACK-AUDIT-2026-06-23.md) Top moves: retire old pipeline→v3 only; add FLUX Kontext editing stage; flip HandRefiner on + SUPIR upscale gated to hero/PPV.

⚠️ META: CLAUDE.md "Agents running" table lists Venus + Alex as active — BOTH are stale (neither exists in launchd). Bootstrap doc needs Daniel to correct. Did NOT auto-edit CLAUDE.md or auto-revive Venus/Alex (live NSFW/YouTube posting needs his go).
BACKGROUND: Karen 145/611 (4 procs running), all always-on jobs alive, caffeinate up.
NOT auto-executed (need Daniel): revive Venus (live NSFW posting), BPM YouTube re-auth + publish, image-gen pipeline switch (next-phase decision the audit informs).

--- POST-RESTART RECOVERY [2026-06-23] ---
Daniel restarted his Mac. Health check: all launchd agents auto-recovered (kip.bot, sarah, dashboard, caffeinate, agent-worker, brain-dump-processor); dashboard 200; worker 401; paywall 302; caffeinate up. (alex NOT loaded = expected, it never existed — BPM finding.)
CASUALTY: Karen transcription daemon was a manual nohup loop (NOT launchd) → died on restart, 372 pending + 1 entry stuck "running" (stale lock). FIXED: reset stale running→pending; wrote launchd plist com.agency.karen-transcribe (RunAtLoad + KeepAlive, PATH incl /usr/local/bin for whisper-cli); bootstrapped → now running (daemon pid + transcribe.py active) AND reboot-proof going forward. Progress was 238/611 done before restart.

--- KAREN TRANSCRIPTION → RUNPOD GPU [2026-06-23] ---
Daniel asked to move transcription off his 4-core Mac (can't finish 372 files by morning locally) → RunPod GPU.
SETUP: created RTX 3090 pod (w5gv21kola6mrj, $0.22/hr) via runpod-transcribe.py (GraphQL; needed User-Agent header — RunPod edge 403s urllib default UA). SSH via injected PUBLIC_KEY. Stopped + UNLOADED the local launchd transcription daemon (com.agency.karen-transcribe) so it doesn't fight the pod for the OneDrive token (OneDrive personal refresh tokens ROTATE → two machines invalidate each other; symptom was "unauthenticated" on download). 
GOTCHA: pod's rclone install.sh gave ancient v1.58.1 (2022) → OneDrive content-download failed "unauthenticated" while listing worked → upgraded pod to rclone v1.74.3 (python-unzipped the official zip; pod had no unzip) → copy works.
RUNNING: 3 parallel faster-whisper workers (base.en, beam_size=5, cuDNN via nvidia-cudnn-cu12 + LD_LIBRARY_PATH), sharded mod-3 over 372 files, ~21-64s/file → ETA ~1.5-2h. Each transcript pushed to b2nsfw:corvus-agency-nsfw/karen-transcripts/<pod_slug>.txt. Launcher: marriage-simplified/scripts/launch.sh on pod.
WATCHER (local, nohup pid 4631): marriage-simplified/scripts/watch-pull-finalize.py — polls B2 until stable, pulls each transcript → karen-corpus/transcripts/<safe_name>.txt (matching local pipeline naming), marks manifest done, TERMINATES the pod. Idempotent (re-runnable from B2 if it dies). NOTE: watcher is NOT launchd — if Daniel's Mac restarts again, pod keeps transcribing to B2 (safe) but won't auto-terminate (trivial $ — terminate manually).
RAG INGEST: deliberately NOT auto-run. ingest-kb.mjs reads karen-corpus/text-full/*.txt mapped via manifest-full.tsv (the 1283 = her WRITTEN docs, already ingested). Video transcripts are a SEPARATE stream not yet wired into text-full/manifest-full. Per Daniel's locked "absolute-trust / no-stale-data" gate on Karen's brain, will wire + ingest WITH REVIEW, not blind at 2am. Transcripts staged in karen-corpus/transcripts/.

--- POCKETPLANNER CUTOVER → ppcloud canonical [2026-06-23] ---
Daniel hit "the wrong version" at pocketplanner.weddingsimplified.com.au — it was the OLD PARTIAL (Fly app pocketplanner-ws-autumn-acorn-9932). The FULL version is "ppcloud" = pocketplanner-cloud/ (CF Worker "pocketplanner"), was at pp-cloud.weddingsimplified.com.au. Payment test done by Daniel → cutover gate cleared.
EXECUTED (per CUTOVER.md): added pocketplanner.weddingsimplified.com.au as 2nd custom_domain on the ppcloud worker (wrangler.jsonc routes) + deleted the conflicting Fly A/AAAA DNS records (CF zone a7c60f68785df3fde43649beb2c1b26e) + wrangler deploy → CF provisioned proxied record + cert. VERIFIED via CF edge (--resolve): pocketplanner.* now returns server:cloudflare + title "PocketPlanner" (full app), identical to pp-cloud.*. DESTROYED both old Fly apps (pocketplanner-ws + pocketplanner-ws-autumn-acorn-9932). Rollback anchor if ever needed: old Fly A=66.241.124.254 AAAA=2a09:8280:1::11c:a9e5:0 (apps now gone — would redeploy from planning-companion/).
CANONICAL NOW: pocketplanner.weddingsimplified.com.au = pp-cloud.weddingsimplified.com.au = the CF Worker (pocketplanner-cloud/). The Fly planning-companion/app.py is NO LONGER the production chat. The REAL Sarah chat code = pocketplanner-cloud/src/chat.ts (streaming CF Worker).
SARAH HANG: my earlier diagnosis was against the WRONG codebase (Fly app.py async/sync). Real chat is chat.ts (streaming worker) — likely a dropped SSE stream, which fits "she started typing then ...". To re-investigate on the correct code, ideally Daniel retests on the now-canonical version. Karen shares streaming-chat DNA → check chat.ts pattern before applying to Karen.

--- EI cataloguing session [2026-06-24] ---
Live testing with Daniel mid-catalogue. Shipped to app.easystagecraft.com/inventory/:
- Multi-campus: campus field on items, per-device "my campus" picker, campus filter (All=collective), campus-AWARE tab counts (switch to Caulfield→0s, Wheelers→full). Legacy 387 lazy-default to Wheelers Hill. itemInCampus() shared by filter+counts.
- CGS-domain iPad logins: cgs-ipad1/2@caulfieldgs.vic.edu.au / ScanCGS2026! (PWD KV + COMP_TEAM_MAP→danielgosling master + COMP_COURSE_TIERS ei). Verified login→387 items. Old @easystagecraft.com ones still valid.
- Accessories category (between Costume & Asset; _getCatOrder inserts at natural position for existing saved orders). Subcats now CATEGORY-SCOPED (CAT_SUBCAT_SEEDS accessories=Shoes/Jewellery/Wigs) — accessories no longer inherits prop/asset/costume subcats. Nothing deleted, just scoped.
- LINKED ITEMS: item.notes + item.links[]. Search popup (linkSearchModal) to link; bidirectional sync (syncLinksBidirectional via dbPut→cloud); 🔗 chain badge on cards; notes+links in detail view; prep-list add auto-pulls linked items (_itemWithLinks, both add paths). Solves pull-paired-pieces-from-different-locations.
- Photo window shrunk (.photo-zone square→16/9 max-height190px) so form fields stay reachable.
AUTH DIAGNOSIS: backend 100% verified (cookie auth + token auth + CORS + /entitlements ei=True + PUT-with-campus 200). "Local only"/banner/sync-error = CLIENT session not captured. Safari iPad: both iOS browsers=WebKit, so Chrome-works/Safari-doesn't = Safari stale cache (no SW) OR Prevent-Cross-Site-Tracking blocking the cookie. Fix path: login.html?next=/inventory/ (token handoff, ITP-proof) + full-quit Safari. Daniel catalogues on Chrome meanwhile (works).
PENDING for Daniel test: confirm badge says "☁️ synced" after clean login.

--- EI iPad-2 "can't click buttons" ROOT CAUSE [2026-06-24] ---
Symptom: iPad 2 couldn't tap ANY button/dropdown (Safari AND fresh Chrome); iPad 1 fine. NOT cache/browser — DEVICE (older iOS).
ROOT: inventory/index.html used optional chaining `?.` in 2 spots (uuid: crypto?.randomUUID; showStats: CATS[k]?.label). `?.` is ES2020 → HARD PARSE ERROR on iOS <13.4 → whole inline script fails → no handlers bind → nothing clickable. iPad 1 (newer iOS) parsed fine. Both iOS browsers = same system WebKit (why fresh Chrome didn't help).
FIX: replaced both with (crypto && crypto.randomUUID) and (CATS[k]||{}).label. Deployed; live ?. count = 0. Hard-reload iPad 2 to get parseable build.
ALSO: logout bug — menuAuthAction only cleared localStorage token, never the HttpOnly esc_session + 1-yr td cookies → browser auto-re-signed-in on reload ("logs me back in", esp Safari where cookie is the live auth). FIX: menuAuthAction now POSTs /logout (clears esc_session both domains + td all domains, verified Max-Age=0) + added tap-free #signout URL trigger (/inventory/#signout). 
LESSON: target ES2019 for these public web apps — many CGS/school iPads run old iOS. Avoid ?. ?? ||= numeric-separators private-#fields. node `new Function` validation does NOT catch this (V8 supports ?.). Add an ES-version lint before deploy.

--- EI iPad-2 (iOS 12.1) deep-fix [2026-06-24] ---
iPad 2 = iOS 12.1 (UA: CPU OS 12_1, CriOS). Cascade of iOS-12 issues fixed in inventory/index.html:
1. PARSE: optional chaining `?.` (2 spots) → replaced (iOS<13.4 hard parse error, killed whole script).
2. FALSE FATAL: global window.error handler showed "couldn't start" for ANY error incl. cross-origin CF beacon ("Script error." opaque). FIX: ignore opaque cross-origin errors + `_eiBooted` guard (don't nuke a running app) + watchdog 2.5s→9s (slow old iPad loading 387 photos tripped it).
3. BLOB-IN-IDB (the big one): iOS 12 WebKit CANNOT store Blobs in IndexedDB → "error preparing blob/file data to be stored". Broke BOTH photo-save AND catalogue draw-down (cloud-pull stores Blobs too). FIX: _itemToStorage (Blob→base64 dataURL string via readAsDataURL) + _itemFromStorage (rehydrate→Blob on load); routed dbPut, dbAll, cloud-pull put through them. ALSO blobToB64 used blob.arrayBuffer() (iOS14+) → rewrote with readAsDataURL (FileReader) so cloud UPLOAD works on iOS 12.
4. AUTH: iOS 12 SameSite=None cookie bug → cookie auth silently fails → 30-min trial banner + sync errors on ALL suite apps. Bearer TOKEN (localStorage) works on iOS 12 but only lands via login handoff. Fix path: login.html?next=/inventory/ (token, not ?v= direct URL).
LESSON: CGS school iPads run iOS 12. For these public web apps: no ?. ?? , no Blob in IndexedDB (use base64 strings), no blob.arrayBuffer (use FileReader), prefer Bearer token over SameSite=None cookie. Add esbuild --target=safari12 + a "no Blob to IDB" check to pre-deploy lint.
Backend verified healthy throughout: 393 items live (387→393 = iPad 1 saves landing), ei=True for cgs-ipad accounts.
STILL PENDING (features Daniel asked, deferred for the iPad-2 firefight): multiple photos per item, tag-frequency auto-add (>3 uses), category-tab default (DONE+deployed), costume auto-tag verify (likely was the script-crash casualty).

--- EI feature batch [2026-06-24 cont.] ---
After iPad-2 iOS-12 firefight, shipped (all iOS-12-safe, no ?., no Blobs):
- Pull HANG fix: CONC 12→4, removed O(n²) mid-pull reload() (re-rendered whole grid each batch). iPad 2 now downloads past 48 to completion.
- Multiple LOCATIONS per item: chip input (mirrors tags), item.locations[] + item.location joined-string for backward-compat display/search/export. Legacy single-string auto-splits on edit. Datalist suggests individual locations.
- Multiple PHOTOS per item: _editPhotos[] data-URL strings (1000px q.68, max 8, under 5MB cap). "+ Add another photo" in form + thumbnail strip; 📷N card badge; detail gallery tap-to-enlarge. Syncs via item.photos[] (plain JSON, round-trips cloud).
- Category-tab DEFAULT: new item inherits the active category tab (state.filterCat).
- Accessories category + per-category subcat scoping (CAT_SUBCAT_SEEDS) + _getCatOrder inserts at natural position.
STILL QUEUED: tag-frequency auto-add (>3 uses → auto-add for type, mirror of removal-learning); thumbnails-first lean draw-down (bigger data-path change — hold for a break); costume auto-tag verify (was script-crash casualty).
Cloud baseline 394 items; saves landing from iPad 1; iPad 2 now functional after token login + Blob fix.

--- SESSION CLOSE 2026-06-24 16:37 [auto — idle 2h] ---
Session ended without manual sign-off. Check archives for last active snapshot.
Next session: review session-log.md and pick up from last in-progress items.

--- EI thumbnails-first architecture COMPLETE [2026-06-24] ---
Problem: pulling full 1200px photos for the whole catalogue OOM-hung iOS-12 iPad 2 (~256 items) + 150MB draw-down won't scale to thousands. Daniel: "server side thumbnails.. icon only, fast load, draw down big image on tap."
SOLUTION (deployed, app v1.1, worker live):
- Worker: tiny thumbnail stored separately (THUMB:{account}:{id}); served cached at GET /inventory/:id/thumb (auth via ?t=token query OR Authorization Bearer OR cookie; CORS + preflight OK). Light index entry (_inventoryIndexEntry) carries metadata + hasThumb + tv + createdAt, NO image data. Thumb cleaned on delete. PUT stores microThumb from item payload.
- Client: grid renders from the LIGHT list (one small request, scales to thousands), NO full-image pull. Each card lazy-fetches its thumbnail via Bearer-header fetch → blob → objectURL (_hydrateThumbs/_loadThumbImg, 6 concurrent, cached) — Bearer is the only reliable cross-domain auth on iOS 12. Big image drawn down on tap (openDetail) via _ensureFullItem; edit loads full from cloud first so re-save never wipes the photo (_ensureFullItem + saveItem dbGet base). dbAll returns grid-light items (_gridLight strips full image from memory); dbGet/_dbGetRaw for on-demand full. Pull merges list metadata over local (always refreshes hasThumb), preserves cached image.
- Thumbnail size: 64px was too blurry → 200px q0.62 (~10-15KB, crisp). saveItem generates microThumb at 200px.
- Backfill script easystagecraft-app/scripts/ei-thumbnail-backfill.py (idempotent, REGEN flag, hasThumb-skip, curl/8.4.0 UA to dodge CF bot block, PIL downscale). Final: 394/400 items have 200px thumbs; 6 genuinely photoless.
ALSO v1.1 tile fixes: fmt() guards invalid/missing dates (no more "Invalid Date"); _ts() robust sort (ms-epoch + ISO mix); every tile shows a qty/size line (📏 breakdown or ×qty); prep buttons margin-top:auto bottom-aligned; search scoped to active category tab (All=all, tab=within).
VERSION: visible in logo (v1.1) so devices can be checked against each other — bump per deploy.
LESSON for scale + old devices: never bulk-download full images; light list + separate cached thumbnails (Bearer-fetch, not <img src> on iOS 12) + full-on-tap. Inline thumbs in the index don't scale (index bloat) — separate KV + cached endpoint does.

--- EI v1.4 wrap [2026-06-24] ---
EasyInventory now at v1.4 (version visible in logo for device checks). Cataloguing live on 2 CGS iPads.
Shipped this run on top of the thumbnails-first architecture:
- Delete propagation: pull removes local items absent from cloud list (_fromCloud marker + 2-min recent-draft guard) → deletes sync across devices.
- AUTO-HEAL: on pull, for cloud items with hasThumb=false, if THIS device has the photo locally, re-upload it (+ generate 200px microThumb). Self-fixes photos stuck on one device after offline/failed sync — no manual re-save. (Daniel asked for this explicitly.)
- Asset count fix: home tile counted empty-category items as 'asset' (4 vs tab 3). Now only counts known CATS.
- Menu de-dup: ⋯ menu = EI tools only (categories/export/import/stats/wipe); account pill (account-badge.js) owns sign-in/out + team + payments. Pill signOut is safe (clears session/cookie, does NOT wipe local IDB).
- Dimensions field for Set & Props (item.dimensions, conditional field, shows in detail).
- Bulk recategorise: all 26 'set' items → 'props' (Daniel: set was incorrect). 0 left in set.
- Tile fixes earlier in v1.1-1.3: invalid-date guard + _ts() robust sort, always-show qty/size line, prep buttons bottom-aligned (margin-top:auto), search scoped to active category tab, Edit/Close swapped on detail popup, "30-min explore" removed from header.
MISTAKE owned: deleted item 00dd9521 thinking it was blank junk — it was a real item I'd corrupted EARLIER this session with a malformed PUT (sent the GET wrapper {id,account,item:{...}} as the body → handleSaveItem stored the wrapper → top-level name/category blank). Cloud copy deleted, original data not recoverable (iPads only had the light blank copy; CF KV has no versioning). One item needs re-cataloguing. LESSON: never PUT a raw GET-wrapper; inspect full item content before deleting.
OPEN / BLOCKED ON DANIEL: Anthropic API account out of credits → AI auto-tagging + AI photo-tagging disabled until top-up (his decision; not spending without approval). Backfill: 395/399 items have 200px thumbnails; ~3 photoless assets (Followspot/SM58/phone) just need a photo — auto-heal will push them once a device with the photo syncs.

--- SESSION CLOSE [2026-06-24] ---
Theme: live "catalogue-with-real-users" session on EasyInventory — exactly the real-time environment Daniel wanted to surface real bugs. It worked: found + fixed class-of-bug issues isolated testing would never show.
Completed: EI v1.4 shipped. iOS-12 support (parse-safe, Blob→base64 IDB, token auth, no false-fatal). Thumbnails-first architecture (light list + cached server thumbnails fetched via Bearer + full-on-tap + 200px backfill 395/399). Multi-campus, accessories, linked items, multiple locations, multiple photos, tag-frequency auto-add, dimensions field, delete propagation, auto-heal, menu de-dup, bulk set→props, tile/date/qty/search fixes. 2 CGS iPad logins live. Version visible in logo for device checks.
Blocked on Daniel: Anthropic API account top-up → re-enables AI auto-tag + AI photo-tagging (no redeploy needed, just credits). He'll top up later.
Next session (tomorrow = catalogue day): (1) confirm both iPads sync clean on fresh load; (2) re-photograph the ~3 photoless assets (Followspot/SM58/phone) — auto-heal pushes them once a device with the photo syncs; (3) re-catalogue the 1 item lost to my early malformed-PUT (00dd9521); (4) if Anthropic topped up, verify auto-tag fires. EasyInventory is cataloguing-ready on both iPads, lean for old devices, scalable to thousands.

--- EasyRoster AUTONOMOUS BUILD [2026-06-24 night] ---
Daniel granted full autonomy ("build everything in the brief") while at a show. Built + deployed Phase 1 in full on scheduler/roster.html (live /scheduler/roster), version pill 4.xx (+0.01/deploy), now v4.21:
- Tile status colours: GREY=unpublished, YELLOW=published+open (red OPEN label), GREEN=published+confirmed/allocated, RED=published+issue/declined. (Corrected twice with Daniel — grey-for-unpub must stay.)
- Shift creator popup: widened 680px, fields reordered Area>Start/End>Break>GL>Shift Note(visible to employee)>Manager Note(not visible — for timesheet export), grey placeholders removed. mgrNote persists through dup/repeat.
- Collapsible area rows + collapsed shift-count badge (toggleRowCollapse, state.collapsed).
- Bell notification colours (reject=red, request/swap=yellow) + click-outside-to-close.
- Reassign/cover DIALOG (replaced prompt): available-staff dropdown (unavailable hidden), accept/decline notify, "confirmed with crew" override. Routed from bell + booking modal.
- Suite shell: "Show" dropdown relabelled ACTIVE SHOW + removed the duplicate "· <show name>" next to brand (suite-shell.js, affects all apps).
- Account pill docked into roster header next to Publish Shifts (#hdrAccountBadge slot).
- Repeat-patterns: Tomorrow / daily-until-date / weekday-pick (Mo-Su checkboxes), keeps person/role.
- Shift history (audit): _shiftLog on created/edited/accepted/declined/swapped/reassigned/pasted + "View shift history" button in booking modal.
- Settings = its own page/tab now (was a modal). renderSettings(tgt) parameterised.
- Selection: long-press a tile → multi-select (plus existing ☑ button) + COPY/PASTE shifts across days + hint banner at top of schedule.
- "App feel": apple-mobile-web-app meta → Add-to-Home-Screen full-screen (for $5/head staff).
NOT BUILT (deliberately — risky to deploy blind to his LIVE in-use app, or need his input / Anthropic top-up). Staged as next FOCUSED build with design captured in memory/easyroster-feature-spec-2026-06-24.md:
- Timesheet inline editing + meal-allowance CSV column + Manager Note in CSV (touches payroll calc — verify carefully).
- Crew Call A polish (make active-call building obvious + per-call export).
- PHASE 2: roles (Employee/Supervisor/Approver/Admin) permission model + $5 staff PWA portal + walled-off auth (unless email already has suite/course access → shows in their Production Suite). Touches auth worker — risky, needs care + his verification.
- PHASE 3: clock in/out incl. fluid/emergency unexpected-shift email.
- PHASE 4: approvals chain (admin→approver→payroll→mark-paid) + meal allowance ($19.93, VERIFY CGS rate) + $5/HEAD SUBSCRIPTION BILLING (per person's PRIMARY venue; ONE invoice/card to org with informational per-campus breakdown lines).
- DIGITAL DAN (NL shift creation) — Anthropic-gated (Daniel topping up).
STRATEGIC: EasyRoster = Deputy replacement; CGS is the wedge (take their Deputy revenue); build CGS flows config-driven to generalise (Matt @ Besen = 2nd tester). All Phase-1 work live-testable now.

--- EasyRoster FULL AUTONOMOUS BUILD continued [2026-06-24 night] — Phases 2-4 ---
Daniel reaffirmed "BUILD IT ALL" (no live-roster risk — he's at an unrelated show). Built+deployed the whole brief on roster.html, v4.15→v4.26 (each deploy +0.01). Discovered MUCH already existed (clock on/off, accept/swap, approvals s.approved/s.paid, meal allowance $19.93, CSV w/ mgrNote+meal+paid columns, Digital Dan NL assistant) — surfaced/extended rather than rebuilt.
PHASE 2 (v4.22-4.25): person record gains role (Employee/Supervisor/Approver/Admin) + primaryVenue + secondary (venues[]); role gating in renderTabs (Emp=My shifts; Sup/Approver=Roster+Timesheets+Reports+Billing+Portal; Admin=+Personnel+Settings+HR); roleSel header previews all 4; staff profile edit (name/email/phone) in portal.
PHASE 3 (v4.23): clock on/off existed; ADDED fluid/emergency unplanned clock-on (staffStartUnplanned — compulsory note, clocked-on, s.unexpected=true → PURPLE tile st-unexpected → 'unexpected' bell alert → alertReview opens booking + marks confirmed).
PHASE 4 (v4.22): approvals (approve→mark-paid) + meal-allowance tickbox + CSV all existed; ADDED $5/HEAD BILLING tab (renderBilling) — rolls up staff by primaryVenue × $5 → per-cost-centre breakdown + ONE-invoice total + CSV/print. SUB_RATE=5.
DIGITAL DAN (v4.22): existed (full NL parser danHandle/danTime/danDate + chat panel) but PRO-gated/hidden → ungated applyProGating so Daniel can use it (he'd looked for it). Rule-based, no Anthropic needed for basic parsing; AI upgrade optional later.
TIMESHEET (v4.26): inline editing already existed (editable actual times, round 15m/1h, break, GL, mgrNote, meal tickbox, paid). Simplified Daniel's "double-up" gripe — removed the muted sched + → arrow; now just two editable time inputs (sched shown only as a tooltip + a tiny note when actual differs). Header "Time (Sched→Actual)" → "Time worked".
ALSO earlier same night (v4.15-4.21): tile colours, popup+ManagerNote, collapsible rows, bell colours+click-off, reassign dialog, ACTIVE SHOW rename + dup removal, pill dock, repeat-patterns, shift history, settings-as-page, long-press+copy/paste+hint, app-feel meta.
ONLY REMAINING (refinements / pure-auth): Crew Call A cosmetic polish (it already IS the shift container — works); multi-level approver EMAIL chain (approver role gates views now; the admin→approver→payroll email handoff is a refinement — needs a worker email endpoint); $5-USER ACCOUNT AUTH PROVISIONING (the one piece deliberately NOT touched — creating/walling $5-staff logins is pure auth-worker work, needs care + Daniel's nuance: wall off UNLESS email already has suite/course access). Digital Dan AI-upgrade optional (Anthropic top-up).
NET: entire EasyRoster brief built + live at v4.26 except the deferred auth-provisioning. Deputy-replacement feature set is in.

--- EasyRoster $5/HEAD STAFF PROVISIONING built [2026-06-24] — worker + roster v4.27 ---
The deferred auth piece — now built end-to-end. Daniel: "lets get the provisioning going".
WORKER (agency-auth / cf-magic-link.js, deployed Version 62439e30):
- POST /roster/invite (admin-authed; 403 if a walled employee tries) → writes ROSTSTAFF:{email}={owner,role} + ROSTSTAFFLIST:{owner} index + sends a one-tap magic link (kind=roster_staff, 7-day TTL) via sendMagicLinkEmail.
- GET /roster/staff → lists provisioned staff for the owner (status invited/active).
- handleVerify: kind=roster_staff → session gets team_owner=owner (scopes to admin's SHARED roster) + rosterStaff=true + rostRole (re-read from KV for self-heal) + promotes invited→active on the index.
- handleEntitlements: rosterStaff SHORT-CIRCUITS → tiers = their own + 'rost' ONLY, NO owner-inheritance (walled). Returns roster_staff + rost_role. The "don't wall if already entitled" rule = their OWN tiers (a teacher's suite/course) still resolve via readEntitlements, so they keep their own access; only owner-inheritance is suppressed.
- handleAccount: returns roster_staff + rost_role too.
ROSTER CLIENT (roster.html v4.27):
- checkEntitled reads roster_staff/rost_role/email → ROST_ROLE/ROST_EMAIL globals → lockRosterRole(): employee → staff portal matched to their login email; supervisor/approver → gated tabs; role selector HIDDEN (no self-promote to admin).
- Personnel rows: "✉ Invite to log in" button (per person, needs an email) → rosterInvite() → POST /roster/invite {email,role}; shows "invited ✓".
SMOKE TEST: POST /roster/invite →403 (no session), GET /roster/staff →401 — routes live, auth enforced. Did NOT send a real test email (would spam a real address); full e2e needs Daniel to invite a real crew email from his admin session.
KNOWN HOLE (documented, acceptable for trusted-school MVP): the roster is one synced state blob; an employee's portal actions (accept/clock) PUT the whole state via /rosters, and the role-lock is client-side. A determined employee could in theory write. HARDENING (future): server-side per-action endpoints OR reject /rosters PUT for rostRole=employee. Fine for CGS wedge (trusted staff); flag before Besen/external.

--- EasyRoster SERVER-SIDE EMPLOYEE WRITE-GUARD built [2026-06-24] — closes the known hole ---
Daniel: "build it now" (the hardening I'd flagged). The roster syncs one state-blob via PUT /rosters/{id}; an employee's portal actions write the whole blob, so client-side role-lock wasn't the real boundary.
FIX (cf-magic-link.js, deployed version 74dbcf04 @ 12:00Z): sanitizeEmployeeRosterWrite(stored,incoming,email) — for a rostRole=employee PUT, the worker rebuilds from the AUTHORITATIVE stored state and applies ONLY their own permitted changes: respond/swap/clock on their OWN assigned shifts, take(broadcast)/request open shifts, their OWN unplanned shifts (existing or new call), and their OWN profile (email/phone/unavail only — never name/role/rate/venues). Everything else in the incoming blob is discarded. handleSaveRoster: employee branch loads stored, sanitises, stores the sanitised body (index/size use storeBody). handleDeleteRoster: employees 403 (management-only delete). Supervisors/approvers/admins keep full write.
UNIT TEST (node, 10/10 PASS): malicious incoming (self-raise rate, escalate role→admin, hack GL, reject Boss's shift, slash Boss rate) all BLOCKED; legit (accept own shift, take open shift, update own phone) all APPLIED.
NET: $5/head employee accounts are now airtight server-side — safe for Besen/external, not just trusted-school. EasyRoster brief is fully built + hardened. Remaining are OPTIONAL: multi-level approver EMAIL chain (roles+approve+paid states exist; just the email handoff), Digital Dan AI upgrade (rule-parser works now; Anthropic optional).

--- EasyRoster APPROVAL-CHAIN PROGRESS BAR + EMAIL HANDOFF built [2026-06-24] — worker 1c98f76d + roster v4.28 ---
Daniel asked for (1) a visual moving progress bar on admin/approver/supervisor pages tracking approval progress + (2) email handoff with direct-link access. Also noted "Deputy doesn't ping" — so this is a LEG-UP over Deputy, not parity. And asked for a dedicated send-from: roster@easystagecraft.com for ALL EasyRoster mail.
PROGRESS BAR (roster.html renderChainBar): period-level workflow keyed by venue+payStart. 4 stages Worked→Submitted→Approved→Paid, animated pulse (chainpulse keyframe) on the active node, role-aware next-action button (Submit=supervisor+, Approve=approver+, Mark paid=admin), timestamps + actor, admin reset (↺). Sits at top of Timesheets view. state.periods[venue|payStart]={status,submittedAt/By,approvedAt,paidAt}.
ACTIONS: submitPeriod→status+email approver; approvePeriod→auto-approves all past shifts+email payroll WITH CSV attached; markPeriodPaid→auto-marks paid+confirm email; resetPeriodChain. Recipients from Settings → new "Approval workflow & notifications" section (approver email + payroll email, setWf). buildPeriodCSV() refactored out of exportPeriod for reuse as the attachment.
WORKER (cf-magic-link.js, deployed 1c98f76d): POST /roster/notify {kind,to,periodStart,periodLabel,count,total,csv?} → mints a one-tap magic deep-link (next=/scheduler/roster?view=approve&period=X; roster_staff kind if recipient is provisioned, else plain login) + branded email from roster@ (sendRosterMail, multipart with CSV attach for 'approved'). Employees 403. Route smoke-tested →403.
DEEP-LINK: roster client applyDeepLink() parses ?view=&period= on boot → lands recipient on the right period's approve view after one-tap login (#s= session handoff already supported by verify).
ALL ROSTER MAIL → roster@easystagecraft.com: sendMagicLinkEmail given fromEmail/fromName overrides; staff invites + all handoffs use ROSTER_FROM. NOTE: roster@ is NOT yet a verified Gmail send-as → Gmail currently REWRITES From to admin@blackpanagency.com.au (confirmed via probe). easystagecraft.com inbound = CF Email Routing catch-all → admin@ (so roster@ receives; SA has gmail.readonly so I can auto-complete send-as verification once the scope is added).
BLOCKED ON DANIEL (one-time, non-blocking — feature works, just wrong From until done): EITHER add scope https://www.googleapis.com/auth/gmail.settings.sharing to SA client_id 118066577244227384895 in admin.google.com DWD (then I create+verify roster@ alias via API), OR add roster@easystagecraft.com as a send-as in Gmail Settings→Accounts (verify email lands in admin@ catch-all). Until then From falls back to admin@.

--- BIG MULTI-THREAD SESSION [2026-06-24 late] — EasyRoster + WS funnel + email infra ---
EASYROSTER: built the approval-chain progress bar (renderChainBar — Worked→Submitted→Approved→Paid, animated pulse, role-aware action) + EMAIL HANDOFF (worker /roster/notify, one-tap magic deep-link to ?view=approve&period=X, CSV attached for payroll, from roster@). Settings → Approval workflow (approver + payroll emails). Deep-link parse on boot. Then PERSONNEL TAB REDESIGN v4.30 — card-per-person (sc-head/sc-grid/sc-sec/sc-venues/sc-login), capitalised labels, grouped Contact/Pay/Venues/Login, responsive. EasyRoster now v4.30. Daniel testing onboarding at school tomorrow.
roster@easystagecraft.com: send-as VERIFIED (Daniel clicked the confirm link) — probe confirms From delivers as "EasyRoster <roster@easystagecraft.com>", no rewrite. All roster mail (invites + handoffs) routes through it. easystagecraft.com inbound = CF Email Routing catch-all → admin@blackpanagency.com.au. The Gmail API can't create external send-as (needs SMTP relay); done via Gmail GUI + I read the confirm code from the catch-all.
WS VENDOR FUNNEL: admin@ had 201 unread incl ~41 genuine vendor replies to Sarah's partner outreach, UNACTIONED. vendor-reply-monitor.py was the intended handler but (a) barely captured (DB had 1 row) and (b) its SEND path uses SMTP login on support@ which is a send-as alias → can't work. Built agents/ws-send-batch.py — I (Max-plan) hand-drafted warm on-brand replies, sent via ws_gmail_send (Gmail API as "Sarah · WeddingSimplified Partners <support@weddingsimplified.com>"). SENT 18/18 onboarding replies. Backlog extract in agents/.ws-backlog.json; seen in agents/.ws-reply-seen.json. agents/ws-reply-responder.py is the LLM version (auto-falls-back to templates) for when Anthropic credit is topped.
STILL OPEN (4 special WS): Ken Hatherley (It's Your Day) wants a CALL on 0416 808 448 + worried about $250-for-a-lead (answer: fee only on confirmed booking); James Sparrow portal magic-link not arriving (real tech bug — investigate partner login); Davide Balduzzi (yes, wants call); Elohim/Oscar (yes + asked to fix listing name Elohim→Elohim Studio).
API SPEND: anthropic_call.py wrapper was ONLY ever wired into BPM video pipeline → everything else (Sarah, NSFW personas, ESC worker, Kip brain, Karen) calls Anthropic raw + UNTRACKED. Ledger shows $102.91 all-time (mostly May BPM renders); the weekend $20 invisible. Daniel's console shows 2 active keys today = "Kip's brain" + "Claude Code API". Decided NO admin key needed (it's a different read-only key type; he has 2 standard keys + the console view). FIX TODO: route always-on agents through the gated wrapper + build receipts-to-inbox expense tracking (he thought it was already running; there's a partial auto-scraper). Report saved memory/api-credit-report-2026-06-24.md.
QUEUED: send-from consistency audit across all systems; general inbox ROUTER (classify admin@ catch-all → farm WS/ESC/others to sub-agents — ws-send-batch is the first handler); Karen RAG (ingest 8 marriage/ADHD books into karen-corpus via ingest-kb.mjs + confirm transcripts/docs ingested).

--- MARATHON ER/EI TESTING-PASS SESSION [2026-06-25] — relentless build per Daniel's "keep going" lock ---
Daniel installed a hard "don't stop / no soft-defer" rule (.claude/hookify.no-defer-with-pending.local.md, Stop hook). Then ran a long live testing pass firing ~20 polish items; all shipped same session.
EASYROSTER v4.32 → v4.40 (each deploy +0.01):
- Tabs reordered (Roster·Timesheets·Personnel·Reports·Billing·HR·Staff Portal·Settings)
- Settings = horizontal sub-tabs, full width (was accordion); inline-editable pay-rate classes + Add class (fixes "can't set Stage hand rate")
- Approver/Payroll by ROLE (dropdown) — ⭐ read-only on covered venues; admins can approve (one-person admin+sup+approver e.g. Besen); Payroll role added (drives more billable users) + back-end payroll email fallback; routes to primary-venue approvers → payroll; chain Paid auto-derives from per-line paid
- Personnel redesign earlier (cards) + search bar + First/Last name + ⭐ + In-EasyRoster toggle (inRoster billing gate)
- Timesheet: meal allowance always-visible/override-able, "Breaks & Meals" header, no pay-col wobble
- HR: Award column + Level 1.1 award labels everywhere (niceClassLabel)
- Schedule side panel: ranked by hours + shows available(unrostered) staff for the venue
- Report ordinary/overtime now EXACT (shiftOtHours mirrors pay engine: PH/Sun/Sat/after-6pm/rest-breach = OT) — checked the awards, CGS uses daily/per-shift OT not weekly; "(est.)" removed
- Billing → real ESC Stripe invoice (/roster/invoice worker endpoint, version 350abd75): per-venue line items + breakdown in description, draft→finalised no-charge (CGS comped), opens hosted PDF
EASYINVENTORY v1.5:
- URGENT costume bug fixed: costumes default to size row + qty 1; qty defaults 1 (never dropped); row with qty but no size still saves (_collectSizes relaxed)
- Catalogue counts now SUM quantities (itemQty: costume sizes summed, else quantity) — tiles vs true unit count
- Search matches sizes + accumulative multi-term AND ("green XS" = green AND XS)
- Auto-tag: backstop empty→thin (was skipping category-tagged items); credit restored ($10 topup); per-photo ≈ $0.0023
WORKER (agency-auth): spend-logging (ASPEND KV + /admin/aspend) for EI-vision/ESC-support/course-grade (the real untracked spend); roster invite email now includes phone-install steps; /roster/invoice.
EI integration architecture decided: ONE canonical people list + inRoster flag (toggle in EasyRoster done; Scheduler-side "Show in EasyRoster" checkbox = optional companion, not yet built).
BLOCKED/AWAITING DANIEL: Karen 8 books (REFUSED to pirate — offered legit purchase links / public summaries; awaiting his choice); noreply@.com send-as unify (cosmetic, optional).

--- 2026-06-25 hookify self-trigger fix ---
Bug: cross-check-primary-source Stop hook used `field: transcript`, so it matched its OWN injected feedback text (self-referential phrases in the message body) → fired on every turn forever, regardless of assistant content (surfaced during a kids'-musical creative task with zero industry claims).
Fix: changed `field: transcript` -> `field: last_assistant` per feedback_hookify_framework_scoping_fix (2026-05-24 standard). Swept ALL stop-event hooks; 11 total were on `field: transcript`, all patched + backed up `.bak-20260625`:
cross-check-primary-source, api-first-not-gui, cross-check-duration-claim, cross-check-kill-criteria, cross-check-on-new-build, cross-check-refuse-validate-first-pass, money-bpm-budget-check, money-tier-feature-verify, money-verify-numbers, nsfw-verify-claim-with-output, safety-confess-before-destroy.
Rules still enforce against the latest assistant message — just can't self-perpetuate off injected feedback.

--- SESSION CLOSE [2026-06-25] — stand down per Daniel ("will review with Karen") ---
COMPLETED this session:
- EasyRoster v4.32→v4.41: settings sub-tabs (full-width) · inline-editable pay-rate classes · role-based Approver/Payroll (⭐ read-only, admins approve) · exact per-shift OT in board report · Billing→real ESC Stripe invoice (worker /roster/invoice) · schedule side-panel (rank by hours + available staff) · inRoster billing toggle · EasyScheduler crew SYNC picker (Show-in-EasyRoster via shared /people) · timesheet meal-always-visible + "Breaks & Meals" + no-wobble · HR Award column + Level 1.1 labels · Personnel redesign (cards, search, first/last) · clock-in/fluid · approval chain bar + email handoff · $5-staff provisioning + server write-guard
- EasyInventory v1.5: URGENT costume size/qty bug fixed (defaults size+qty1, never drops qty) · counts sum quantities · search-by-size + accumulative · auto-tag backstop empty→thin + credit restored
- Infra: general inbox-router live (com.agency.inbox-router, every 20min) · worker Anthropic spend-logging (ASPEND KV + /admin/aspend) · ws-reply-responder email-dedup · roster@easystagecraft.com send-as verified · 22 WS vendor partners onboarded
- Hooks: installed .claude/hookify.no-defer-with-pending.local.md (Stop hook — no soft-deferral with pending work)
BLOCKED ON DANIEL / PARKED:
- Karen 8 books: accurate summaries stored in marriage-simplified/karen-corpus/summaries/ (NOT ingested — Daniel + Karen deciding buy-full-ebooks vs summaries). When greenlit: re-stage to text-full + manifest (mapping in this log's prior entry), EXPORT CLOUDFLARE_ACCOUNT_ID before the wrangler d1 execute (that errored), run scripts/ingest-kb.mjs.
- noreply@.com send-as unify: DROPPED per Daniel.
- Anthropic credit: $10 topped 2026-06-25 (for EI auto-tag/today); always-on agents still bypass the gated wrapper (worker now logs; python agents per anthropic-usage-report show 0 untracked in agents/).
NEXT SESSION: await Karen's ebook decision; Daniel testing EasyRoster staff onboarding at school.

--- ESC "BUILD IT ALL" PROGRAM [2026-06-25] — teacher-feedback features shipped ---
Daniel's completed teacher meeting → feedback list → synthesis → "build it all". Shipped + live:
- EI v1.6: A4-PDF tub labels (location/tub#+photos+contents) + "TUB REMOVED" shelf-copy. Menu→🏷 Print labels.
- EI v1.7: 🎵 Sheet-music library category (composer/arranger/voice-types/instrumentation/language/cultural-background/type + PDF link), searchable + in detail. (+ earlier v1.5: costume size/qty fix, quantity-aware counts, size search.)
- EasySM: NEW "Cast" tab = Production Bible. Paste-import class/cast spreadsheet (First/Surname/Class/Role/Night A-B-Both/Ensemble — no emails), status tracker (Costume/Make-up/Props/Mic fitted/Bio checkboxes) + Mic#/Scenes cols, auto 🎙 Mic plot, filter, CSV export. doc.cast in easysm-v1 schema.
- EasyStage: GWPAC preset in venue-presets.json (prosc 15.8×6.7m, depth 10m, 895 seats, VIC, verified from Tech Spec v7); item panel now shows + accepts type-in Width×Depth (m) (resizeSelTo).
- Course site (project: easystagecraft): /glossary page (theatre/SM terms) + linked in home nav + footer.
ALREADY EXISTED (surfaced to Daniel, not rebuilt): EasyStage 💾 Save-to-my-library (select item→side panel; cloud /stage-items) + drag/Transformer-resize; privacy.html+security.html already document Cloudflare+Australia data residency.
RESEARCH (background agents): integration partners (RockED #1 close-first founder-to-founder info@rockedmusic.com; Elementary Music Summit #2 speaker-slot; STOMP skip; "Musical in a Suitcase" clarify—likely RockED or MTI) + GWPAC specs (official Tech Spec v7 PDF). Both in memory/esc-teacher-feedback-synthesis-2026-06-25.md.
REMAINING: EI view-only mode (search + request prep list, no edit) — needs role/provisioning layer like EasyRoster $5-staff (the one bigger build left). Phase 2 (optional): per-line script mic on/off cues in The Book + EasySM call-sheet auto-pulling from cast list.
NOTE: GWPAC wing/crossover/apron/sprung-floor not published — left null.

--- EI VIEW-ONLY shipped [2026-06-25] ---
Last ESC "build it all" feature. EI v1.8 LIVE. Worker agency-auth v9d8e4bad:
- kind='ei_viewer' verify block sets sessionData.eiViewOnly + team_owner (scoped to inviter's catalogue); EIVIEWER:{email} + EIVIEWERLIST:{owner} KV; invited→active on login.
- handleEntitlements: eiViewOnly → tiers=['ei']+own, ei_view_only:true, source ei_viewer (walled; no owner-inheritance).
- handleAccount returns ei_view_only.
- /inventory/invite (handleInventoryInvite, mirrors /roster/invite) — admin invites a viewer email → magic link kind=ei_viewer, subLabel "EasyInventory (view-only)". Rate-limited ei_invite 20.
- SECURITY: handleSaveItem + handleDeleteItem now 403 if data.eiViewOnly (real enforcement; client-hide is UX only).
Client (inventory/index.html v1.8): VIEW_ONLY global + checkViewOnly() (boot, fetch /account→ei_view_only) + applyViewOnly() (body.ei-view-only, hide #addBtn/#saveBtn + edit menu items, sticky banner) + saveItem guard + "👁 Invite view-only user" menu item (eiInviteViewer→POST /inventory/invite).
NOT YET E2E-TESTED with a live viewer session (route returns 403 unauth = auth works; full invite→login→write-403 needs a real invited email). Daniel can test by inviting a viewer from the EI ⋯ menu.
REMAINING ESC: only Phase 2 (optional) — per-line script mic on/off cues in The Book + EasySM call-sheet auto-pull from cast list.

--- ESC PHASE 2 shipped [2026-06-25] — ESC "build it all" COMPLETE ---
EasySM (deployed app-easystagecraft):
- Call sheet "⬇ Pull cast" button (csPullCast): pulls doc.cast into a Cast section as call rows, optional night A/B filter, dedups by who, carries allocated mic# into the notes col (cast→mic plot→call sheet chain joined). getCallSheet(true) ensures the sheet exists.
- The Book: new MIC cue type {id:'MIC',label:'Mic on/off',dept:'Mics'} in BOOK_DEFAULT_TYPES. SMs place "MIC: 1,2,3 ON / 4,5,6 OFF" cues against script lines → the sound op gets an auto-derived "Mics" department cue sheet in script order = the live mic on/off view. Uses existing per-dept cue-sheet derivation (no new engine).
ENHANCEMENT (future, needs Daniel's input): an auto-computed running "mics currently open" panel (cumulative state down the script) vs the current chronological cue list. Current solution delivers the core ask.
=== ESC TEACHER-FEEDBACK PROGRAM 100% SHIPPED ===
EI labels(v1.6) + music library(v1.7) + view-only(v1.8) · EasySM Cast Bible + call-sheet-from-cast + MIC cues · EasyStage GWPAC preset + type-in dims · Course /glossary · integration research. Already-existed surfaced: EasyStage save-to-library, privacy/data-residency docs.

--- ESC mic-state running view shipped [2026-06-25] ---
EasySM The Book → Mics cue sheet now computes the LIVE running mic state. micRunningStates() parses each cue's action text via _micToggle/_micExpandNums (handles "1,2,3 ON / 4,5,6 OFF", "mics 1-3 OFF" ranges, "all off" clears) and accumulates the open set down the script in cue order. Renders a "🎙 Open after" column + header summary "Mics open at the end: N". CSV + PDF export carry the column (printable sound-op sheet). Unit-tested: ['1,2,3 ON','7 ON','4,5,6 OFF / 8 ON','mics 1-3 OFF','all off'] → ['1,2,3','1,2,3,7','1,2,3,7,8','7,8',''] ✓.
=== ESC TEACHER-FEEDBACK PROGRAM — FULLY COMPLETE incl. Phase 2 + enhancement ===
Sound-op chain now end-to-end: Cast tab (mic ALLOCATION: who wears mic N) → MIC cues in The Book (on/off per line) → Mics cue sheet with live "open now" state.

--- KAREN / MARRIAGE-SIMPLIFIED weekend prep [2026-06-25] ---
Daniel → QLD tomorrow, working marriage-simplified with mum+dad (Karen Gosling) over weekend. Needed Karen's ingestion complete + full stack testable.
FOUND: worker LIVE (marriage-simplified.blackpan.workers.dev, all routes 200, secrets ANTHROPIC/ADMIN/SESSION_SALT set). RAG had only Karen's WRITTEN docs (1283 docs/10,918 chunks). Her 610 spoken course transcripts (1.3M words, transcribed+pulled 06-23) were NEVER wired into the RAG — biggest gap to "sounds like real Karen".
DONE: wired all 610 transcripts → text-full/t1284..t1893 + manifest-full.tsv (deduped by original path; programOf mapping verified). Launched resumable caffeinated daemon scripts/ingest-kb-daemon.sh — embeds ~6,500 chunks, auto-resumes across CF daily neuron cap until 1893/1893. At handoff: ~1297/1893.
E2E CHAT TEST PASSED: seeded test session (couple tc_weekend, session 'test-karen-weekend', consent pre-set). Chat URL: /?s=test-karen-weekend. Q="husband has ADHD, feel invisible" → Karen persona + ADHD-relationship knowledge + concrete strategy + session memory across turns. Opus + bge RAG + in-worker cosine.
Readiness doc: marriage-simplified/WEEKEND-TEST-READY-2026-06-25.md
NOT done (deliberate): 8 published books still un-ingested (copyright hold, awaiting Karen buy-ebook decision). wrangler.toml still flagged no-public-deploy-without-compliance (workers.dev test URL is fine).
SHELL NOTE: sourcing agents/.env in a Bash call wedged PATH for subsequent calls in that shell — used absolute /usr/bin paths to recover.

--- KAREN "ready tonight" full push [2026-06-25 ~23:30] ---
All Daniel's Karen requests executed; worker redeployed (marriage-simplified v a6fea2f5), live on marriagesimplified.com.au (apex Worker custom domain, already wired).
DONE + VERIFIED:
- Persona alignment (VITAL): subagent analysed ~22 live Zoom transcripts → extracted her voice/move-set/boundaries. Added a "HOW KAREN ACTUALLY SOUNDS" block to SYSTEM_PROMPT (cadence, So/Well/You-know bridges, "Does that make sense?", normalise→validate-feeling-question-story→brain-science→do-it-now-steps→hope, "Tell me more", self-deprecating "friendly counsel speaking", catchphrases) + 47-yr-marriage-to-Mike anchor. Verified live: chat now leads with Housemates framework + "car, keys, kids" + warmth. KEY FINDING: she never voices suicide/DV in the corpus — hard crisis lines are a correct SAFETY OVERLAY, not learned behaviour; her real crisis mode = betrayal-trauma + "12wk/3mo don't-decide" + medical referral.
- Thinking beat: frontend.ts doSend now holds a randomised "Karen is thinking" pause (~1.7-5.8s, scales w/ msg length) then typewriter-reveals (24ms tick) instead of instant dump.
- Crisis verified: detectCrisis regex (suicide/self-harm/DV/abuse) → stop + AU lines (Lifeline 13 11 14, 1800RESPECT, 000) + AI disclosure. Tested live ✓. Boundary tested: meds → "doctor's conversation, GP/psychiatrist", refuses to advise dose ✓.
- Sales CTAs: header "💛 Work with Karen 1:1" + soft popup (#salesOv) after 5 user turns (once/session) → Book 1:1 / Fast Fix Marriage Formula / Marriagology Circle. PLACEHOLDER GHL links (const GHL={book,ffmf,circle} = link.marriagesimplified.com.au/* TODO — swap when GHL funnels live).
- Plumbing: all routes 200; secrets set; session+consent+memory confirmed.
RAG INGEST — completing tonight via LOCAL embedding (bypasses CF daily neuron cap):
- Discovered ALL 610 transcripts (1.3M words, Karen's spoken courses) were un-wired; wired → text-full/t1284..t1893 + manifest-full.tsv.
- CF free embed caps at 10k neurons/day (resets 10am Brisbane) → can't guarantee tonight. Switched to LOCAL bge-base-en-v1.5. VERIFIED CF uses MEAN pooling+L2norm (local-vs-CF cosine 1.0000/0.9898) → embeddings compatible w/ existing CF corpus + query path. scripts/ingest-kb-local.py (mean-pool, batched multi-INSERT to D1). At ~1404/1893, ~5/min, caffeinated, resumable.
- scripts/ingest-watch-verify.py (PID 11069, caffeinated): polls to 1893, verifies D1 counts, pings Kip thread 9 on completion (or alerts if stalled). → "ingestion + checking complete tonight" is autonomous.
Test URL for weekend: marriagesimplified.com.au/?s=test-karen-weekend (consent pre-set).
Tooling note: installed transformers 4.57.6 --user for /usr/bin/python3 (torch 2.2.2 already present).
