Doc rule: update this file and APP_BREAKDOWN.md on every change. Mirror of the .md — same numbering.
1What it is
A retention command center for Shopify wellness brands. It watches every customer for churn risk — supply running out, stalled shipments, skipped renewals, unresolved negative tickets, first-order stalls, email disengagement — ranks them in an at-risk queue with a plain-English "why flagged," and makes the fix one click: guided cancel-save scripts, call/text scheduling with topic routing, portal phone calls with auto-transcribed notes, and zero-margin loyalty-points save offers.
Business context: runs on Flip My Life's real store (flipmylifenow.myshopify.com, selling since Aug 2023 — tenant id t_verdant is a leftover label from the demo era; the data in it is all real). Architected multi-tenant from day one so it's a sellable SaaS at exit. The aggregated voice-of-customer data (pain points / win factors per churn reason) is itself the pitch asset.
Positioning: Gorgias is where you talk to customers, Recharge is where subscriptions live, Rivo is where loyalty points live — Flip CMS sits across them: see the churn risk, act on it in one click. Wellness-specific differentiators: the replenishment ring with real-dose inference, shipping as a churn signal (a stalled shipment is churn-in-waiting and never counts as incoming supply), and the wellbeing-first medical guardrail (retention offers structurally impossible on medical reasons).
Naming
The app is Flip CMS. "Chardizy" was the original builder's codename — it survives only in technical identifiers that are expensive to rename (the chardizy/ code folder, the D1 database name). Flip My Life Wellness is the brand skin on the front-end. The stack: subscriptions = Recharge, loyalty = Rivo, support = Gorgias, logistics = Flexport.
2How to run it
Production:https://flip-cms.flipmylife.workers.dev — Cloudflare Workers + D1, no local server. First visit per device: append ?key=<value> (key in ACCESS_KEY.txt) → 30-day cookie. Dashboard at /, Connections at /connections, dose facts at /products, live config at /breakdown. A cron syncs everything every 30 minutes and rebuilds the queue cache.
Deploy: tests must be green first — npm test then npm run deploy (gate on the exit code; never chain blindly).
Local dev (Node ≥ 22.5, zero npm dependencies):
cd "F:\Claude Code\Flip CMS\chardizy"
npm start # ONE server on :8787 — same code path as the Worker
npm test # 70 tests (node --test)
Production data lives in D1 (database chardizy); local dev uses data/chardizy.db. Tenant comes from the x-tenant-id header (default t_verdant) — session-derived tenancy is the planned auth upgrade.
3Architecture
Flip CMS/
APP_BREAKDOWN.md/.html · GO_LIVE_CHECKLIST.md · ACCESS_KEY.txt
chardizy/ ← code folder (historical name — the app is Flip CMS)
worker.js ← Workers entry: key gate · API+webhooks on D1 · assets ·
30-min cron (sync all services → rebuild queue cache)
wrangler.jsonc ← D1 binding (db `chardizy`) · assets · cron · APP_KEY
server/
db.js ← ONE async facade: node:sqlite (local) + D1 (prod)
schema.js (15 tables) · seed.js (local demo only) · service.js (queue/cache/vips/config)
api.js ← fetch-native handleFetch — same code on Workers + local
lib/ ring.js (+3-day delivery buffer) · risk.js (injectable weights) ·
saveflow.js · routing.js · upserts.js (identity unification, no ghost minting)
integrations/ registry.js (connection hub: chunked resumable syncs, cursors in sync_state)
shopify.js (client-credentials tokens, read_all_orders) ·
recharge.js (cursor pages, skips, self-registering webhooks) ·
rivo.js (per-id lookups — member id == Shopify customer id) ·
gorgias.js (tickets + live threads) · flexport.js (webhooks only) ·
adapters.js · voice.js (seams; endpoints pending)
test/ 70 tests: core · api E2E · integrations · connect
web/ flip-my-life-retention.html (LIVE dashboard: queue/profiles/VIPs, responsive)
connections.html · products.html · dashboard.jsx (legacy prototype)
data/chardizy.db local-dev SQLite only — production data lives in D1
Layering rule:lib/ and service.js never touch vendor specifics; integrations normalize external payloads into internal row shapes. The same handleFetch + db facade run identically on Workers (D1) and local node (SQLite).
4Data model (15 tables — D1/SQLite, tenant scoping in the query layer)
precomputed queues + VIPs, cron-refreshed — why the dashboard loads in ~200ms
External-ID columns (v0.2.0):customers.shopify_customer_id/.recharge_customer_id, products.shopify_product_id/.sku, orders.shopify_order_id/.recharge_order_id, subscriptions.recharge_subscription_id, shipments.flexport_shipment_id, tickets.gorgias_ticket_id — the keys that make webhook replays and repeated syncs idempotent. Older persisted DBs upgrade in place via MIGRATION_COLUMNS.
5The engines (all numbers)
5.1 Replenishment ring — lib/ring.js
supplyDays = qty × servings_per_unit ÷ dailyDose; ring counts down from delivery + 3-day buffer (people don't start the day the box lands). Dose inference: median of servings÷gap across consecutive deliveries (needs ≥2), clamped 0.1–5.0/day. API exposes dailyDoseUsed vs labelDose.
5.2 Risk engine — lib/risk.js
Scale (v0.7.1): scoring is aggregate-first (per-customer order counts + last-delivery anchors from one GROUP BY; full rows only for the trailing 150 days) and scores only customers with ≥1 order / subscription / ticket. The dashboard never runs it live — the 30-min cron precomputes into queue_cache (~200ms serves; ?fresh=1 rescores in ~26s).
Signal
Weight
Fires when
supply_depleted
30
ring ≤ 0 days AND nothing dependable in flight
shipment_stalled
25
in-flight shipment, no carrier scan ≥ 48h
first_order_stall
22
exactly 1 order, day 30–45, no repurchase
repeat_pauser
22
≥2 skips/pauses in 90 days — the "essentially pausing" pattern
negative_open_ticket
20
open ticket with negative sentiment
renewal_skipped
20
a skipped charge ≤60d on a non-cancelled sub (skips live on charges; the sub stays "active")
subscription_paused
18
sub paused (detail carries reason)
supply_low_no_order
15
ring ≤ 25%, no reliable inbound, next charge won't beat depletion
lapsed_buyer
12
bought before, no live sub, quiet 60–365d — the win-back pool
email_disengaged
8
opened 0 of last 3 emails
Score = Σ weights, ×1.1 if LTV ≥ tenant top-decile, capped 100. Tiers: high ≥ 70 · med ≥ 45 · low. A stalled shipment is excluded from incoming supply. Shipping/supply signals carry a Gorgias cross-reference when support already has a replacement ticket. All weights/tiers editable live on /breakdown. Live example: top of queue = stalled + depleted + skipped + repeat-skipper = 100/high.
5.3 Cancel-save plays — lib/saveflow.js
Reason
Label
Offers
results
Not seeing results yet
Free coach session · dosing plan · 2-week pause auto-resume
noPush — pause immediately · full refund. No retention offers, structurally.
other
Something else
listen & log · route · offer pause
Guardrails proven by tests: the medical list contains no retention offers so a discount is impossible, not discouraged; loyalty points can never unlock it. The points lever fires only on price with ≥ $5.00 redeemable, as the lead zero-margin offer; fabricated amounts are rejected.
Every outcome auto-writes tagged notes (reason→pain_point, saved→win_factor, follow_up→note, cancelled→feedback) feeding the insights rollup.
5.4 Routing — lib/routing.js
topic → rule role → first member with role → fallback member → members[0]. Unknown topic → admin. Seeded: product_results→Dana (coach), shipping/billing→Miguel (CX), sub_changes/cancel_save→Sasha (retention).
5.5 Insights — service.insights
GROUP BY exact note body per tag → pain points (mentions + last_heard), win factors, feedback, save-rates by reason+offer with ltv_saved_cents. Works because auto-notes are canonical strings — free-text notes will fragment rollups (B12).
6API
Path
Purpose
GET
/api/queue
at-risk queue from the precomputed cache (?category=subs|shipping|supply|winback|unhappy, ?limit=, ?fresh=1 rescores, computed_at exposed)
GET
/api/vips
best customers: tenure + (live sub OR ≥2 orders), LTV-ranked, last-touch guard; cached (~200ms), ?tenure= runs live
Routes that still don't exist:/api/voice/* (token, TwiML, recording callback) and the booking-link consumer page. The voice adapters are ready and hardened; the HTTP surface is P1.
7Integrations — ALL FIVE LIVE + fully backfilled (2026-07-06)
All services connected on /connections, maintained by the 30-min cron. Backfills DONE: Shopify ~274k orders (to Aug 2023) · Recharge full history incl. 18,496 skipped charges · Gorgias ~34k tickets · Rivo points for the top ~10k LTV customers.
Service
Credentials
Webhook security
Sync
Webhook ingests
Shopify
store domain + Dev Dashboard Client ID/secret (client-credentials → 24h tokens, auto-minted; app+store must share an org). read_all_orders scope required — without it Shopify silently serves only 60 days
HMAC-SHA256 base64, timing-safe — Notifications-page secret or app client secret
customers + orders, since_id cursors, resumable
orders/* → customer+product+order · fulfillments/* → shipment (delivered stamps the ring anchor) · customers/*
Recharge
API token · webhook client secret
HMAC-SHA256 hex, timing-safe
cursor-paginated: identity phase → active subs → history → skipped charges (newest-first, self-healing)
order/created → confirmed order · subscription/* → status + events · charge/updated → skip events (charges are the truth — subs stay "active"). Webhooks self-register via the button
Rivo
Developer Toolkit token
shared-token gate
two-phase: bulk member pages (API caps ~250 pages) + per-id refresh of top-10k LTV — member id is the Shopify customer id; list filters are silent no-ops
points/updated → rivo_* columns
Gorgias
domain · email · REST API key
shared-token gate
tickets, cursor-paged (⚠ re-walks the archive each pass — incremental marker on the board)
ticket create/update → tickets; heuristic sentiment (LLM = drop-in upgrade). Full threads fetched live on click
Flexport
API token (expires yearly)
shared-token gate
none — Logistics API 2024-07 has no order-list endpoint; tracking flows via Shopify fulfillments
exception→stalled (churn-in-waiting)
Twilio + Whisper
not on Connections yet
X-Twilio-Signature (timing-safe)
—
needs /api/voice/* routes + browser SDK (P1)
Sync cursors:integration_settings.sync_state persists per-resource cursors (written per page — crash-safe); chunked runs report remaining; caught-up passes are cheap incremental pulls. Proven Workers envelope: ~2 pages (500 all-new rows) per run.
Ingestion design (lib/upserts.js + registry.js): every row matches on its external id first — replays and re-syncs never duplicate. Customers unify across platforms (Shopify id ⇄ Recharge id ⇄ email — ~45.5k identities merged); identity-less payloads never mint customer rows (the 227k-ghost lesson). Order writes recompute LTV + first_order_at.
8Front-end
8.1 The dashboard — web/flip-my-life-retention.html (LIVE on real data)
Design language "The Flip": midnight ink ground, coral→gold flip gradient reserved for the moment that matters, mint for saved/healthy. Space Grotesk display · Inter body · IBM Plex Mono data. The signature mechanic: a saved customer's card physically flips (3D rotateY) from at-risk to saved. Reduced-motion + focus-visible respected.
Fulfillment tab — stuck-order radar: skipped charge (high→Recover), stalled carrier (high→Reship), customs (watch→Heads-up); 3 counters; Open jumps to queue.
Insights tab — 3-column tagged-note rollup (pain / wins / feedback), populated live by saves and calls.
Team tab — routing display (static).
Modals — SaveModal (diagnose → say-this + offers with gold points-lever on price + wellbeing banner on medical → outcome); ScheduleModal (channel → topic with routing preview → slot or self-book link); CallPanel (simulated: dialing pulse → live timer + scripted transcript → "Whisper transcribing" → 3 auto-tagged notes).
Fully wired to the live API (v0.4.0+): queue with category chips, lazy detail, VIPs tab (60s refresh, real points/tiers), Gorgias thread click-through, live saves/notes/touchpoints, URL-addressable full profiles (#customer=<id>), and a complete mobile/responsive build (full-screen detail sheets, touch targets, iOS input guard). Tech debt: React 18 UMD + Babel standalone from CDN — a Vite build is on the board.
8.2 Legacy prototype — web/dashboard.jsx
First clickable prototype (light sage "Chardizy" theme). Has features the branded UI dropped: Calls & notes tab (touchpoint queue + tagged note pad), Shipping radar, Cohorts retention heatmap, Playbooks (toggleable automation rules). Port these forward rather than losing them.
Per-service cards with credential fields (secrets masked, round-trip safe), Save & test with honest error surfacing, Sync now with result counts, and for Recharge a Register webhooks button (the server registers its own receivers — no manual setup). Auto-refreshing webhook activity table (ok / rejected / error) below.
8.4 Duplicate file
Flip CMS/flip-my-life-retention.html (zip root) is byte-identical to the web/ copy — treat web/ as source of truth (B10).
9Tests — 70/70 passing (2026-07-06, Node 24.18.0)
File
Covers
core.test.js
ring math + delivery buffer, dose inference, all signal firings incl. repeat_pauser + lapsed_buyer, stalled-isn't-supply, LTV multiplier, medical structural block, points lever, note tags, routing fallbacks
api.test.js
E2E over real HTTP: ranked queue, detail bundle, loyalty + lever, touchpoint routing, save→notes→insights, medical 400, tenant isolation
integrations.test.js
Flexport exception→stalled, Recharge HMAC + cadence + order map, Rivo normalize (both payload shapes), TwiML, Twilio signature, voice grant
◐ README refreshed for v0.2.0; the referenced Postgres spec (chardizy-data-model-spec.md) is still missing — ask the builder or regenerate
B9
✅ FIXED v0.4.0 — hardcoded UI stats removed with the live wiring
B10
OPEN — duplicate branded HTML at zip root — drift risk
B11
✅ FIXED v0.2.0 — ids now prefixed crypto.randomUUID()
B12
OPEN — insights GROUP BY exact body — free-text notes fragment rollups; needs canonical topic classification
UI ↔ API contract mismatches
✅ ALL RESOLVED v0.4.0 — the dashboard rewrite made API slugs the single source of truth end-to-end (M1–M5 can't recur by construction).
Security / compliance
Interim access gate live (shared APP_KEY, 30-day cookie; /webhooks/* exempt — signature-verified). Real auth = Cloudflare Access on a custom domain + session-derived tenant (kills the spoofable x-tenant-id header) — top security item
Integration credentials stored as plaintext JSON in D1 (masked in every response, never logged) — move to encrypted-at-rest / secret store as the app grows
CORS * all headers/methods
Health-data posture open: encrypt notes at rest, RBAC, retention/deletion policy — a diligence asset at sale
SMS opt-in gating (TCPA) already enforced in code ✅
12TODO board (prioritized)
Shipped: live dashboard · Workers+D1 deploy · all 5 services connected · full backfills · Retention 2.0 signals incl. skips · queue cache at scale · mobile · full profiles · webhook self-registration · Rivo points · ghost purge + minting guard. The demo→product jump is done.
P0Trust & access — before handing this to the team
Real auth: Cloudflare Access on a custom domain replacing the shared key; session-derived tenant (kill the spoofable x-tenant-id header) — guards customer PII + integration credentials
Gorgias incremental sync marker — the cron re-walks the ~34k-ticket archive every 30 min for zero new data
Real front-end build (Vite) instead of Babel-in-browser; self-host fonts
P1Close the loops that are 80% built
Rivo points→$ conversion — Rivo doesn't expose per-customer redeemable value; derive it from /rewards once and the zero-margin "Redeem $X" save lever lights up
12bDepartment report roadmap (batch 20 — the Insights tile dashboard)
The Insights tab is now a dashboard of nine department tiles. Fulfillment is live (Delivery Performance); each other tile becomes its own cinematic report — comprehensive enough to be the only thing you bring to that meeting — plus a department login whose account can actually do that department's work.
Dept
The report
The login can DO
Data source
Fulfillment ✅ live
Delivery performance, carriers + pie, claims scoreboard, money left on the table, loss/gain
File claims, reship, export Excel/PPTX/PDF
Flexport + Shopify + mined CS disputes
Sales
Revenue by channel (store · wholesale · Amazon · distributors), AOV, cohort repeat rate, sub vs one-time mix, top movers
Every channel side-by-side; discount/price experiments; wholesale account book
Shopify + Recharge today; Amazon/distributor feeds when connected
Marketing
CAC by channel, ROAS, attribution wired into the profitability engine's break-even ("an ad dollar becomes a profitable customer on order N / day D"), creative performance
Manage/pause ads + budgets; VOC pipeline — CS-mined pain points & wins become the messaging brief
Ad platforms (to connect) + cac_cents scaffold + dispute/notes miner
Finance
Store P&L, margin by product, refund exposure, per-customer ROI + store-average break-even, claim recoveries, LTV:CAC
Set per-variant COGS, review refund outcomes, books-ready exports
The profitability engine (costBasis/orderProfit) — already computes it
Marketing leans on customer service by design — who better to know what customers want than the team already talking to them; the systems are one, so tagged VOC notes flow straight into campaign briefs. Department logins ride the existing role system (roles already gate tabs; a department account gets its tile's report + that action surface).
13Changelog
2026-07-13 — v0.59.0 BATCH 32: STUDIO VIDEO EDITOR — OUR OWN TIMELINE · Instead of Higgsfield's editor, FLIPPER got its own 🎬 Editor tab. An edit = ordered clips over gallery assets (ai_edits, non-destructive in_s/out_s/duration_s); start fresh or seed from a shotlist (rendered shots land in scene order). ▶ Play all steps through the cut (videos seek in→out, stills hold); timeline strip scaled to clip length; inspector trims/reorders/removes; ➕ Add from the gallery; ✨ Generate missing shot stages a pre-approval card (model + prompt + cost → ✓ Approve) and drops the clip at position. 🎞 Export = FCP7 timeline.zip for Premiere/Resolve, now honoring the trims as in/out frames. Single track v1, grows from here. 106/106.
2026-07-13 — v0.58.0 BATCH 31: PRE-APPROVAL ON EVERY GENERATION · Standing rule (memory: generation-approval-rule): nothing generates automatically. Shotlist 🖼/🎬 clicks now STAGE a review card — model (⭐ default, swappable), editable final prompt, refs, est cost — and only ✓ Approve spends; cancel = zero jobs (verified). Runner + lander buttons show est cost; auto-chaining banned. Fixed: StudioAI tail duplication (since batch 29) truncated. 105/105.
2026-07-13 — v0.57.0 BATCH 30: LANDER ENGINE + REAL VIDEO ANALYSIS + THE METHODOLOGY · Tyler's claude.ai project chats read (signed-in pane): the proven ad pipeline extracted → tyler-storyboard-methodology memory + prefs. Standalone lander-engine Worker (KV edge serving, hostname→tenant multi-brand, bearer-gated /publish, view counts) — ad traffic fully isolated from the app; 🚀 Publish per lander w/ slug → live URL. Real video analysis: browser scene-cut detection samples 2 frames/scene → Claude runs a VFX-supervisor breakdown of the actual footage before writing the page. 105/105.
2026-07-13 — v0.56.0 BATCH 29: MODEL CHOICE + PREFS + HIGGSFIELD-STYLE RUNNER + AD LANDERS · OpenAI provider (GPT Image 2 — Tyler's storyboard pick, sync path) · preference engine (⭐ team picks seed the runner + shotlist Board/Shoot defaults; per-prompt model select; learns from project results) · runner v2: aspect/param chips generated from the Higgsfield snapshot (no more raw JSON), reference slots w/ gallery picker + upload, private assets chain as data URIs · 🛬 Ad landers: Claude sees the ad creative and writes a branded animated landing page continuing its story — public at /l/:id, CTAs deep-link Shopify checkout (cart permalink w/ chosen variant). 105/105.
2026-07-13 — v0.55.0 BATCH 28: SHOTLIST DIRECTOR + HIGGSFIELD WATCH + NLE EXPORT + TRASH · The Seedance Shotlist Director rebuilt as a living pre-production doc: shotlists import from the skill's JSON into ai_shotlists/ai_shots, and /report/shotlist/:id renders a cinematic-slides deck (dot rail, arrows, fullscreen, fully responsive) where every prompt has ⧉ Copy · 🖼 Board (Seedream frame) · 🎬 Shoot (Seedance — i2v off the attached board); generations poll in-page and the slide fills in as footage lands. Scene checkboxes are server-side (team-shared); re-imports carry done + renders forward. 🎞 Timeline export: FCP7 XML (Premiere + Resolve) + media-fetch scripts in a zip — an editor opens a pre-assembled project. 📡 Higgsfield watch: paste a fresh models_explore probe → diff (new/retired/changed) against the stored 74-model snapshot; 🩺 tool health validates every slug on the provider's live page (11/11 OK at ship). 🗑 Trash: soft delete → Trash view → restore / empty-now / 30-day cron auto-purge (R2 bytes really deleted). 104/104.
2026-07-13 — v0.54.0 BATCH 27: STUDIO AI TOOLS — THE IN-HOUSE HIGGSFIELD · The Studio's new ✨ AI Tools view calls generation models directly — no Higgsfield credits. Probe first: the Higgsfield MCP catalog enumerated free of charge → 74 models, ~60 third-party with public APIs (full mapping in HIGGSFIELD_CAPABILITY_MAP.md). The sidestep: fal.ai + Replicate adapters (integrations/ai-providers.js, queue submit → poll → harvest) + Claude for copy. 12 launch tools (FLUX.2 Pro, Nano Banana Pro/2, Seedream 5.0 Pro+Edit, Seedance 2.0 t2v/i2v, Kling 3.0 Pro, Veo 3.1 Fast, ElevenLabs TTS, SeedVR2 upscale, Claude copy), each tagged with the Higgsfield tool it replaces; the catalog is data, not code — slugs/templates/costs editable in-app. Bring-your-own-AI: per-member keys (🔑 My keys) fall back to the house key on /connections (fal + Replicate cards added); every job records key_source + est cost, header shows 30-day house-key spend per member — the charge-back groundwork. Storage: new flipper-studio R2 bucket — finished media copied out of provider CDNs, streamed/downloaded from the app, provider URL kept for 🔗 chaining (image→video/upscale). Tables ai_tools/ai_user_keys/ai_jobs/ai_assets; routes /api/studio/ai/*. Verified in-browser; clean no-key gating. 109/109.
2026-07-13 — v0.53.0 BATCH 26: VISION ALT TEXT — CLAUDE READS THE IMAGES · Alt-text drafts now come from what's in the picture: new Claude (Anthropic) connector on /connections + lib/vision.js (image fetched via Shopify CDN at 800px → base64 → Messages API, claude-opus-4-8, ≤125-char storefront alt; source pages ride along as context). 👁 Scan per item + 👁 Scan images (next 10) batch; vision drafts wear a 👁 badge; SVGs keep filename drafts with a reason. Review-first: nothing touches Shopify until ⚡ Apply — scan → skim/edit → apply → the action completes itself. All 368 images ≈ a few dollars at Opus rates. Clean 400 gate when no key. 106/106.
2026-07-13 — v0.52.1 BATCH 25: SELF-COMPLETING ACTIONS + PRODUCT-MEDIA ALT TEXT · Closing an action's last open item (any path — ✓/✕/⚡301/⚡alt) flips the action to done automatically; reopening an item pulls a done action back to in progress (syncITAction on every item mutation). Alt text covers product gallery images now: unified setShopifyImageAlt — Content→Files first, then product images matched by filename (one products fetch, cached per batch) and PUT on the product; fix-detail records which path applied. 105/105.
2026-07-13 — v0.52.0 BATCH 24: IT BOARD PHASE 2 — ALT-TEXT AUTO-APPLY + SYSTEM HEALTH + COMPRESSION + STATUS RE-EXPORT · ⚡ Alt text applies itself: per-image "⚡ Apply" writes the editable draft onto the Shopify file (new shopifyGraphql + setShopifyFileAltByUrl: files() search by filename stem → fileUpdate; product-media URLs return a reason and stay guided) + "Apply drafts to next 25" batch — each click approves one batch; applied items auto-fix with the alt recorded; action reclassified auto. 🩺 System health strip (/api/it/health): per-connector card — green/amber/red, last sync, last error, 24h webhook ✓/✕ with the last problem on hover. 🗜 In-app compression on 100KB+ items: Compress proxy-fetches (host-whitelisted), canvas-re-encodes to WebP ≤100KB in the browser, downloads correctly named. ⬇ Status re-export: /api/it/export-seo rebuilds the SEO team's own Technical Actions List with the Status column from the board + Progress ("N of M fixed") + as-of stamp + a Fixed-items log sheet (every 301/alt applied, with timestamps — verifiable proof of work). Verified in-browser; 104/104.
2026-07-13 — v0.51.0 BATCH 23: THE IT / WEBMASTER BOARD — SEO IMPORT + PIPELINED FIXES + LIVE 301s · New IT tab (admin): upload the SEO team's workbook → the Technical Actions List becomes a pipelined board — actions classified ⚡ LIVE FIXES / 🧭 GUIDED / 🌐 EXTERNAL, grouped by priority, each with the team's description + how-to-fix, a numbered pipeline (steps tagged IN-APP / OPEN↗ Shopify-admin deep link / OUTSIDE) and its detail worksheet as trackable items (✓/✕, paginated). ~9,000 raw rows → ~700 real items (broken links grouped per dead target with sources + anchors; images grouped per file with a drafted, editable alt text). Live automation: one-click 301s — type the destination, hit ⚡ Create 301, the redirect lands in Shopify on the spot (click = approval; existing redirects detected; homepage refused), item auto-fixed. Idempotent re-imports (fixed items never resurrect). Plumbing: dependency-free lib/xlsx-read.js (ZIP + DecompressionStream + sheet XML, Workers-safe), it-seo.js classifier (12 kinds), it_actions/it_action_items, /api/it/*. Fixed in passing: local dev mangled ALL binary bodies (uploads and downloads) — node adapter now byte-clean both ways. Insights IT tile live (open actions · high priority · items to fix → #tab=it). Verified on the real July 2026 review: 12 actions / 690 items, counts matching the SEO team's own numbers; UI walked in-browser. 103/103.
2026-07-11 — v0.50.0 BATCH 22: THE STUDIO (BASECAMP REPLACEMENT) + TRICIA'S TRUE COUNTS + TILE DATE RANGES · THE STUDIO — Basecamp replacement in-app (one shared brain): tables studio_projects/tasks/events/messages; a Studio tab for members + admins with ✓ Tasks (project columns, add/complete/assign/due), 📅 Calendar (month grid, click-to-add — the creative/marketing calendar), 💬 Team chat (# team + per-project channels, 8s polling). Basecamp connector on /connections: imports projects → to-dos (assignees email-matched) → schedule entries, idempotent by basecamp_id; cron re-imports every 6h max, manual sync always runs. 🎨 Creative Studio tile on Insights (gold, LIVE — IN-APP: open tasks · due this week · calendar 7d · chat today → #tab=studio). New /api/studio/* + E2E test. Tricia's true header: her replacement (RYIYS0AIZX, delivered 02-07) shipped to zip 52631 vs her stale 52625 address — attached; the crisis block counts ALL paid orders ("received 2 of 10") + shows "N replacements sent", while the trigger/"still out" only consider orders ≤120d old (unstamped pre-Flexport history can't false-flag). Tile reporting windows: master 30/60/90/custom switch above the tiles + per-tile overrides; /api/report-summary?days= serves any window through the report's own gather (60 + customs build once then cache); the Fulfillment tile carries its window into the full report and leads with profit missed. Batch-21 heal tally: 2,354 of 2,768 unstamped orders were really delivered (stamped; ~414 genuinely in transit/lost). 102/102.
2026-07-11 — v0.49.0 BATCH 21: STARTING PROFIT + DELIVERED-STAMP ROOT CAUSE + SUB PRICES IN SWAP + PPTX = PDF · Money slide reframed: new per-order starting profit (paid − tax − COGS − fee — profit if it had landed clean) + headline quartet: PROFIT MISSED (Σstarting − Σactual, leads, coral) · could have made · actually made · winnable; new Starting profit column between Loss/Gain and Flexport cap; Excel matches. Cache v19. #274888 "no confirmation" root cause was systemic: the Flexport Order.* webhook stamped only the id and dropped deliveredAt on the floor — every confirmation since the CSV walks stopped was lost unless someone opened that order (March–May ~0.4% unstamped → June 5.9% → July 94.5%). Handler now stamps delivered_at from the payload (order-number match, Flexport-id fallback) + a heal walk re-checked all 2,768 unstamped orders against the API (#274888 → delivered 07-09). Sub-swap prices: dropdown now shows the real subscription price — variant price minus the Recharge plan discount ("$49.48/sub (10% off $54.98)"); the A/B twin FLIP 7 SuperShake is UNLISTED in Shopify (only reachable by id) — swap-catalog now pulls plan-referenced products missing from the catalog by id; the twins differ only by plan discount (10% vs 20%) — visible now. Cache swapcat3. Healthy ≠ flagged: green "Healthy right now" + white rest, no "Why flagged" label. PPTX = PDF: deck.js rewritten — centered, label pills, accent half-headlines, the same 10 slides (incl. the pie page) and columns; python-pptx: 10 slides, zero out-of-bounds shapes; PDF re-verified live (money slide fits the quartet + column). 101/101.
2026-07-11 — v0.48.0 BATCH 20: DEPARTMENT TILE DASHBOARD + PDF FIXED ON REAL DATA + CLAIM STATES + THREAD NAV + CRISIS PROFILES + REPLACEMENT AUDIT · Insights → the department dashboard: everything old removed; nine big tiles — Fulfillment LIVE with a slide-1-style mini summary (new GET /api/report-summary reads the warm 90d cache) opening Delivery Performance; eight placeholders (sales · marketing · finance · inventory — consumption's future home · customer service · social · affiliates · IT 🧙) with planned-content blurbs; roadmap in §12b. PDF cut-off actually fixed — verified on REAL live data (fixtures fit, live didn't): with 8 real carriers the print-forced pie + table overflowed and flex-centering clipped BOTH ends; JS-paged tables printed 10 tall rows. Fix: pie gets its own print-only page, table face forced on, print row-caps (fact-check 6 · claims 6 + full-width reject-reasons wrap · money 8). Verified: live payload → headless Chrome → PyMuPDF — 10 pages / 10 slides, all rasterized, nothing clipped.Claim state machine (#278018 was in transit yet showed File claim): claimState() → filed ("View claim ↗ · status" — never "File" twice) / open ("File claim ↗ · by date") / not_yet ("⏳ unlocks if undelivered by promised-by") / expired ("window closed") — the order view only ever shows the action you can take. Thread ‹ › nav pages through that customer's conversations (grayed at ends). 🚨 DELIVERY CRISIS banner (tricia@ — 1 of 8 received, still subscribed, never refunded, header looked calm): profiles compute delivery_crisis (≥2 aged-undelivered, or <60% rate on 3+) and wear the coral banner. Break-even on the economics card: "✓ Became profitable on order N — day D" / "✕ Not yet"; store-wide average lands with CAC. Dawn P's replacement (SYMYMTGQZE) + the audit: sweep matcher gained an orders-zip fallback (her address field was stale); her row had ALSO landed on the wrong duplicate account ("Dawn Porter"/ebw1060 vs "Dawn P"/360medspa who owns #270838 — merge candidate) — fixed, $51.25 reship now folds in (verified in D1). All 976 portal replacements audited: 674 attributed (name-verified; a zip+recency-only re-audit pass was reverted — same zip ≠ same address), 303 non-customer samples, ~101 goodwill/late sends outside any order's 120d window (charge to lifetime P&L, correctly). rpt_dlv% caches purged (data changed, payload stays v18). 101/101.
2026-07-11 — v0.47.0 CUSTOMER PROFITABILITY + PER-ORDER P&L + CLAIM-WINDOW GATING + PROFILE SPEED · Full profile gets a Customer-economics card: Total spend beside Profitability (green/red) + margin. Profit = paid − tax − COGS − shipping − refunds − replacements (reuses the loss/gain engine; #242403 reads +$113.83 here too). Off a daily-cached tenant cost_basis (no added round-trips); CAC scaffolded for when ad channels connect. Per-order +$/−$ chip on every order-history row. Claim-window gating (new lib/claims.js, 30d from delivery / 45d from order — from our own filing history): report winnable now counts only orders still in-window; the rest go to a new EXPIRED tile (live 90d: 59 of 86 never-filed already expired). Order view's File-claim button hides once the window closes. Profile speed: parallelized the customer-row fetch in bundleFor (−1 round-trip/open) + added idx_touchpoints_owner. Cache v18. 101/101.
2026-07-11 — v0.46.2 LOSS/GAIN NAILED: REAL PER-RESHIP SHIPPING + MULTI-RESHIP · Tyler re-checked #242403: the reship "double whammy" was assuming the replacement's shipping = the original order's fee, but an expedited reship costs more (original $46.66 vs reship $64.05, confirmed via Flexport API). Fix: new fx_direct_orders.cost_cents (backfilled for all 976 portal replacements) + money query counts EVERY reship ($0 Shopify + fx_direct portal) and sums each one's own real shipping; multiple reships stack (triple-whammy). Verified live #242403 = +$113.83, Tyler's exact figure ($322.27 total cost vs $436.10 paid ex-tax). Badge shows "📦 replacement ×N". The per-variant COGS entries were correct all along — the whole gap was the reship shipping. Cache v17. 101/101.
2026-07-11 — v0.46.1 PDF = THE RIVO RECIPE + MULTI-LINE COGS FIX + ECON SLIDE RETIRED · PDF back to browser print done right (the Rivo hub recipe): @page{size:1600px 900px} = the deck's design viewport in CSS pixels, slides locked to one viewport each — printed pages are pixel-identical to the screen; verified headless-Chrome→PyMuPDF, 9 pages 16:9, rasterized + eyeballed. Server-drawn PDF deleted. Loss/Gain fixed (Tyler hand-checked #242403): tax now excluded from revenue AND COGS sums across ALL line items (new orders.line_items, 558 failure orders backfilled) — formula reproduces his math exactly; remaining delta = input values (app COGS $105.78 vs his $258.22; Flexport cost $46.66 vs his $64.05) flagged for the Products page. Cheapest-vs-UPS slides dropped; churn attribution ("Subs lost ≤60d") moved onto the Carriers slide; economics stay in Excel. Cache v16. 101/101.
2026-07-11 — v0.46.0 SERVER-DRAWN PDF + LOSS/GAIN MODEL + REMEDY TOTALS + CARRIER CHURN · PDF rebuilt: print-to-PDF clipped slides, so ⬇ PDF now downloads a server-drawn PDF of the exact PowerPoint deck — slide layout extracted to shared lib/deck.js, rendered by both lib/pptx.js and new dependency-free lib/pdf.js (16:9 pages, native text/tables/bezier pies); pypdf-validated, 10 pages. Money slide → Loss/Gain: Cost-to-us column/tile gone; Loss/Gain = (paid − refunded + claim payouts) − (COGS + fees + reship); headline duo = Loss/Gain + Winnable; net-loss filter. Remedies TOTAL per carrier on the econ slide (answers "DHL can't be $0.05/parcel" — total + per-parcel both shown, net of claim payouts). Carrier churn attribution: "Subs lost ≤60d" column — live 90d: 22 subscribers cancelled after a failure; DHL 7 of 11k parcels, UPS 5 of just 1.6k — worst rate. Cache v15. 102/102.
2026-07-11 — v0.45.1 CARRIER ECON REFRAMED: THE TIER PITCH vs REALITY · Tyler's correction: every customer order rides the standard cheapest-carrier auction — we never buy 1/2/3-day except our own manual replacements/samples. Quantified live (90d): 394 manual expedited shipments (41 $0 reships + 353 portal replacements) = 1.2% of 33,513 parcels; UPS carries just 4.9%, only auction wins — "the tier pitch oversold UPS" now leads the slide. Model numbers stand (always computed on the standard flow); blanket-UPS verdict gains the selection-bias floor note (UPS's $18.19 is from lanes it WON — forcing all lanes prices worse); new ask: "quote a real standard-tier rate card." New `manual` + `ups_share_pct` fields; Excel + PPTX match. Cache v14. 101/101.
2026-07-11 — v0.45.0 BATCH 18: CARRIER ECONOMICS + SLIDE NAV + REAL PDF + POWERPOINT EXPORT · The "cheapest option" counter-model: new slide computes TRUE cost per parcel = real Flexport fee + remedies (refunds/reships − claim payouts) per carrier; fees de-biased by a 1,079-order sampling walk (DHL $12.03 → UPS $18.19; failure-only sample had UPS at $21.06). Table + ranked recommendations (✅ prefer / ⚠ monitor / ❌ pull) + optimal-mix savings + generated "ask Flexport" talking points; AK/HI excluded (815/819 are USPS — rep's rule confirmed). Excel `Carrier economics` sheet. Slide nav: titled dot rail, click jumps anywhere, active dot tracks scroll. PDF fixed for real: exact 11×8.5in page boxes — headless-Chrome verified 10 pages / 10 slides, zero clipped. ⬇ PowerPoint export: dependency-free `lib/pptx.js` (shared `buildZip`), native dark 16:9 deck, editable text/tables/pie shapes, `/report/delivery.pptx`; validated via python-pptx + structural test. Cache v13. Cron warm moved FIRST + made gentle (one stalest window per run) — heavy syncs were eating the 15-min budget and the invocation got evicted before the end-of-loop warm ever ran; warm-first fixed it (4 windows in ~15s, v13 verified live) but 4 back-to-back gathers at cron time briefly overloaded prod D1 (webhooks 500'd; Recharge redelivers, sweeps backstop). Live 365d verdicts: blanket-UPS −$507k/yr · recommended mix +$190k/yr · DHL best true cost everywhere · UPS has the WORST failure rate — the data clears cheapest-first on failures and reframes the Flexport ask around fee mix. 101/101.
2026-07-10 — v0.44.1 BATCH 17: FULFILLMENT TAB RETIRED + MONEY SLIDE FITS + LOSS/GAIN + CALM BUTTONS · Fulfillment tab removed (redundant — delivery report + At-risk shipping bubbles cover it). Consumption off Products page (Insights only). Money slide fits one screen: explanation condensed to one wide line + scoped #money compaction (own 1300px table cap — the global 920px cap was double-wrapping the 11-column rows — 12px nowrap cells, tighter paddings); verified a full 10-row page fits 1600×900. Loss/Gain column (profit lost − cost to us) between Cost-to-us and Flexport cap: +green / −red; Excel matches. Recovered: $0 = red, recovered = green. Button language enforced (Tyler screenshot): coral→gold gradient = negative-indicating only (cancel-save flow + brand marks); every regular button app-wide → calm mint primary; Insights report link → surface + mint border. 99/99.
2026-07-10 — v0.44.0 BATCH 16: TAX OUT + CARRIER PIE + PDF EXPORT + COHORT CONSUMPTION + MONEY-TABLE v4 · Tax out of the money model (a wash): profit lost = paid − COGS − fulfillment. Carrier pie on the carriers slide: pill buttons flip table ⇄ pie — slice size = share of parcels, color = transit speed, caption calls out when the slowest carrier is also the most used; PDF carries both views. ⬇ PDF button next to Excel: print-to-PDF tuned to the exact live look — landscape 11×8.5, one slide per page, dark kept. Consumption split by cohort (cache v2): subscribers ~30.7d/bag (117,576 pairs) vs one-timers ~45.1d (7,366) — blend hid a 14-day gap; subscriber pace off REAL orders so skips + moved dates are baked in; Insights = blended + split, Products = split. Cohort tag in JS (SQL join blew D1 CPU). Money table v4: Profit-lost column out (metric stays), Cost-to-us green under profit / red+⚠ over, badges say full/partial refund, new Carrier + Flexport says columns + carrier filter, Our response = fact-check treatment (⚖ claim chips). Report cache v12. 99/99.
2026-07-10 — v0.43.1 "OPENED" ≠ DAMAGED: DISPUTE CLASSIFIER FIX · Tyler caught #265222 flagged damaged when it was a 30-day-guarantee product return (opened & tried, didn't agree with his health conditions). Root cause in lib/dispute-rx.js: the open|opened damage token matched "unopened" (agent's "confirm if any product is unopened for a return label") and benign "opened and tried", the damaged branch ignored the dissatisfaction guard, and DISSATISFACTION_RX didn't know "not the right fit / doesn't agree with me / satisfaction guarantee". Fix: split damage into STRONG (shattered/torn/leaking — always kept) vs WEAK ("arrived open", \b-anchored so it can't match inside "unopened") gated by dissatisfaction, guard broadened to product-sentiment phrases only (excluded "not happy / a refund / return" — lost-package customers say those too). Re-scanned all 3,421 flags: cleared 185 false damaged, recategorized 9 damaged→missing (wrong-address), touched 0 genuine missing. Damaged 508→314; #265222 off the money slide. New test/dispute-rx.test.js. 99/99.
2026-07-10 — v0.43.0 MONEY MODEL v3: COST-TO-US vs PROFIT-LOST · Reworked the money slide around Tyler's two numbers. Cost to us = every COGS/fee/fulfillment on the original order + all remedies (refund given, plus reship COGS + a 2nd fee — the double whammy). Profit lost = the margin those orders would've made: paid − COGS − fulfillment − tax (new orders.tax_cents, backfilled from Shopify total_tax for all 646 failure orders, 454 non-zero). Cost cell turns red + ⚠ when cost outgrew profit ("underwater") — new headline count, "underwater" filter, "most profit lost" sort. Headline is now a trio: Cost to us · Profit lost · Winnable (dropped the "structural / we eat" framing — the Flexport cap already shows in its own column). Excel rebuilt to match. Cache v11, purged + redeployed. COGS still the labeled 30%-of-retail estimate until per-variant COGS is entered. Also: 👤 View Profile now closes the order + opens the profile in one click (was: needed an X first). 94/94.
2026-07-09 — v0.42.2 PER-VARIANT COGS + DELIVERED≠LOST GUARD + VIEW-PROFILE ON ORDERS · COGS field moved to per bundle variant on Products (money model reads order's variant × qty). Delivered orders can't be "lost": #240794 (delivered 04-20, stale lost status, $0 cost) fixed — healed 6 shipments, every lost query now requires delivered_at IS NULL, order-view self-heal closes stale shipment + fixes display. Failure set → 364; winnable $16,437 / structural $5,402. 👤 View profile button on order-view header. Cost model = incremental P&L (refund = margin+goods, reship = extra COGS+fulfillment). 94/94.
2026-07-09 — v0.42.1 MONEY MODEL v2: NON-SHIPPING REFUNDS OUT + RESHIP DOUBLE-WHAMMY + OUR-RESPONSE COL · Classifier now excludes product-dissatisfaction refunds ("didn't like it / no results / allergic") from all shipping metrics — kept STRONG signals (lost/stolen/wrong-address/damaged), suppressed WEAK "haven't received" when it's really a product gripe. Re-scan: 4,651→3,421 flagged. Reship = double whammy: costs COGS + 2nd fee, not retail (new products.cogs_cents + COGS $/bag on Products page; 30%-of-retail estimate until entered). Our response column (💸/📦) on slide + Excel. 365d: winnable $17,053, structural $5,473 (mostly UNFILED claims, not structural — at ~30% COGS Flexport's 40% covers reship product). COGS-sensitive; enter real COGS to finalize. 94/94.
2026-07-09 — v0.42.0 CRACKED FLEXPORT'S CLAIM FORMULA → HONEST MONEY MODEL · Reverse-engineered from our approved claims: payout = 40% of affected-goods retail + fulfillment fee (exact on #169833: 0.40×$235.84+$25.89=$120.23). Whole loss → 40% of order; single missing/damaged item → 40% of that item (~$25 for a bag). New fulfillment_fee_cents (763 orders) + refunded_cents (133). Money slide rebuilt: max recoverable = 40%×retail + fee, split into WINNABLE $20,019 (never-filed 16.4k / denied 2.8k / under-filed 0.8k) vs STRUCTURAL $18,769 (the ~60% cap never covers). 365d cost us $38,597, recovered $3,218. Cache v9. 94/94.
2026-07-09 — v0.41.0 MONEY LEFT ON THE TABLE SLIDE · New report slide quantifying dollars lost to Flexport failures three ways: never claimed · denied · underpaid. Grounded in actuals — cost = real refund $ (new orders.refunded_cents, backfilled from Shopify for the 133 dispute/claim/lost orders, partials exact) + replacement value; recovered = approved payout; left = cost−recovered. 365d: $39,531 left on the table vs $3,444 recovered (305 never-claimed / 59 denied / 61 underpaid). Interactive filterable table + hero $ on slide 1 + Money-left Excel sheet. Cache v8. 94/94.
2026-07-09 — v0.40.1 READABILITY: NO MORE GRAY-ON-GRAY · Palette-level contrast fix (--muted→#A9AEC2, --muted2→#858BA4 — old muted2 was ~2.4:1); delivery block: DELIVERY label wears the sentiment color, undelivered status line in gold, carrier + placed/fulfillment line white. Plus the "✨ new version — Refresh now" watcher (5-min poll + tab focus). 94/94.
2026-07-09 — v0.40.0 FAIR WHEEL + 🏖 VACATION + CONSUMPTION STATS + ORDER ◀▶ + 0.35s PROFILES · Auto-assign wheel persists its cursor — one ticket per person in strict rotation across runs; 🏖 vacation toggle (Team tab) sits members out; schedules = follow-up. Consumption & seasonality live: real days-per-bag vs label from reorder pairs (Chocolate Courage: 19.7d real vs 15d label, 54k pairs, winter 15.8d vs summer 22d) + monthly rhythm bars — on Products + Insights. Order sheets: ◀ ▶ / arrow keys through a customer's history w/ neighbor prefetch. Profiles open in 0.35s (tickets SELECT * hauled full bodies — now 1.5k tails). Profile-wide "Product used by" retired (per-sub 👥 roster). 94/94.
2026-07-09 — v0.39.1 REFINEMENT PASS · Card scrolls with the page (bubble list keeps its own scrollbar); win-back why-text trimmed ("They already cancelled."); cancel-intent placeholder removed (button appears only when it applies); 📭 NEVER CONTACTED banner; household modal portaled to body (fixed-position vs animated ancestors); force-update overlay centered. 94/94.
2026-07-09 — v0.39.0 COHERENT WHY-TEXT + NAMED ROSTER + SPLIT SCROLL + SLIDE NAV · Win-back why-text: "They already cancelled — the job is a reason to come back" with old signals as history (no more "churn-in-waiting" on cancelled customers); category now flows through mapQueueItem → cancel-save on every win-back for real. 👥 Named household roster: editable names (Person 1…), tick products per person, one person on two products counts in both; saving re-derives the count maps. Profile opens instantly from the card (SWR cache). Queue list + card scroll independently. Slideshow: arrow keys + fixed ↑↓ buttons + wheel pages slide-by-slide (tall slides scroll to edge first); claims-paid tile fits (rounded $, wrap-safe). 94/94.
2026-07-09 — v0.38.0 DESIGN NOTES: CANCEL LINE + WIN-BACK v2 + 👥 PER-PRODUCT + RETURN GATES + SLIDESHOW POLISH · Red "✕ Cancelled sub MM-DD-YY" banner atop fully-cancelled profiles (product left, old frequency right). Win-back card: sub badges, clickable latest order, cancel-save always on, case-normalized statuses (no active subs in win-back). Detail card scrolls first (sticky + own scrollbar). ⟳ Force update on the At-Risk chips. 👥 people-per-product stepper on sub rows → product_users JSON feeds ring/cadence. Returns: delivered-only (UI+API+adoption); #261004's wrong bubble cleared. Slideshow: ⛶ fullscreen, center-snap + dvh (no cut-offs), dark dropdowns, claim approved/rejected filter, whole-deck hero tiles. 94/94.
2026-07-09 — v0.37.0 DISPUTE QUALITY v2 + DAMAGED + PHOTOS + INTERACTIVE TABLES + NEW BUBBLES + LIVE VERDICT · Classifier exclusions from Tyler's false positives (scoop accessory, self-changed address); word-boundary snippets. ARRIVED DAMAGED category (re-scan: 4,246 never received + 400 damaged) split everywhere + customer photo links mined from Gorgias attachments. Fact-check + Claims tables: 10-row pages, ‹ › arrows, carrier/category/response/verdict filters, sorting; conversations load on demand. Trend charts scroll horizontally (all buckets). Worst column dropped; "Lost (Flexport)"→"Lost". ⏭ Frequent skippers + 🚚 Shipping issues bubbles (exclusive, heat > shipping > skipper > churn > win-back). Subs-cancelled badges show the cancel date. Order sheets show Flexport's live verdict — "⚠ POTENTIALLY LOST — 29d past promised" (#261004) + delivered_at self-heal. 94/94.
2026-07-09 — v0.36.0 CLAIMS SCOREBOARD + AUTO REPORT SWEEP + INSTANT TRENDS + THE 14M-ROW INDEX · Claims via the reports API (Claims-Claims_Submitted — no REST endpoint exists): fx_claims backfilled — 134 claims · 71 approved $3,443.75 · 60 REJECTED $3,283.18; new ⚖️ scoreboard slide + claim chips on fact-check rows + 186 corroborated disputes never claimed. Weekly cron sweep auto-ingests Orders+Claims reports (future portal replacements land automatically, $0 cost). Trend toggle instant (3 granularities from one daily scan, client-side switch). Perf: claims→orders join read 14.4M rows (missing idx_orders_flexport) + blob-through-sorter fixed → all windows cold in 2.1–2.9s, warm ~0.1s. 94/94.
2026-07-09 — v0.35.0 TREND GRANULARITY + CLICKABLE CONVERSATIONS + FLEXPORT-DIRECT REPLACEMENTS · Trend slide gains daily/weekly/monthly buttons (?interval=); Excel Trend carries all buckets. Fact-check table: Placed column + snippets are click-to-read popups with the full conversation; Excel CS-disputes sheet holds the full conversation per cell. 976 Flexport-portal-created replacement orders discovered (in Flexport, never in Shopify), 659 customer-matched by name+zip → new fx_direct_orders table; reship corroboration checks it too. 90d: 128 disputed · 110 carrier-confirmed · 43 corroborated. 94/94.
2026-07-09 — v0.34.0 DISPUTE CORROBORATION: REFUNDS + REPLACEMENTS = HARD EVIDENCE · New orders.financial_status (Shopify refunded/partially_refunded) captured on every upsert + webhook, store-wide backfill walk; reship detection = $0 order same customer ≤120d after. Dispute columns read "26 (9 ✓)" (corroborated subset in green) on carriers + cohorts; fact-check slide gains Our response (💸 refunded · 📦 replacement sent) and sorts corroborated first. Excel: Corroborated columns + Refunded/Replacement flags on the CS-disputes sheet. 94/94.
2026-07-09 — v0.33.0 DELIVERY FACT-CHECK (FLEXPORT vs CS) + REPORT POLISH · Ticket bodies mined for never-arrived / wrong-address / "marked delivered" language (tickets.dlv_*: flag, attributed order, evidence snippet). New 🔎 Fact-check slide: "Flexport says delivered MM-DD" vs the customer's actual words; carriers + cohorts tables gain a CS disputes column next to Lost (Flexport) (⚠ when lost=0 but disputes exist); Excel gains a full CS-disputes worksheet. Cron sweep mines new tickets; local backfill covers the 45k corpus. Regex excludes carrier boilerplate ("Haven't received your package?"). Polish: trend axis MM-YY, DELIVERR excluded (internal transfers), cohorts columns mirror carriers. 94/94.
2026-07-09 — v0.32.0 FLEXPORT-ONLY REPORT + EXCEL DRILL-DOWN + CARRIER ON-TIME/LATE/LOST · Every report query requires flexport_order_id — no ShipBob in any window (365d verified: trend starts 2025-10, the cutover). ⬇ Excel export: multi-worksheet .xlsx built in the Worker (dep-free lib/xlsx.js): Summary · Carriers · Trend · Cohorts · every late order w/ customer (2,364 @ 90d) · all 372 undelivered · lost. Carrier table: worst-shipment column → on-time / late / lost split (DHL 87.9/12.1/5 vs Veho 99.7/0.3/0). Cron delivery sweep asks Flexport's deliveredAt first (Shopify fallback). Temp ops account deleted. 94/94.
2026-07-09 — v0.31.0 REPORT CUSTOM RANGES + FLEXPORT-ONLY + CRON PRE-WARM + ORDER SEARCH · ShipBob-era comparison removed from the delivery report (Flexport-only per Tyler). Custom from→to date pickers beside the preset buttons (60-min cache per range). Cron pre-warms all 4 preset windows every 30 min — picker clicks are ~0.8s warm instead of 4–8s cold. Order-# search shows the order itself: 📦 row in the dropdown opens the full order sheet; the owning customer still listed. Variant walk COMPLETE (133,892 orders) — consumption stats unblocked. 94/94.
2026-07-09 — v0.30.0 DELIVERY REPORT V2 + TRANSIT TRUTH SWEEP + CRON MEMORY FIX · Stale at-risk chips traced to the cron rebuild dying on the Worker's 128MB cap (bulk ticket query hauled 48k full bodies) — explicit columns + tail-trimmed bodies for open/recent only; rebuild 25s, Linda's card clean. 484 transit outliers verified against Flexport: 480 corrected (#240405: 80-ish days → real 17.1d), the 52 still >30d are Flexport-confirmed real. /report/delivery v2 (button atop Insights): 30/90/180/365d picker, carriers ranked worst-first (alias-merged), era + cohort comparison, verified slowest-10, aging undelivered (372), LOST count — 10min cache per window (~0.8s warm), print = PDF. 90d: 34,251 delivered · 5.8d avg · DHL slowest major, USPS worst slow-share, Veho/Jitsu the stars. 94/94.
2026-07-09 — v0.29.0 POST-DEMO: SHIPMENT TRUTH + REFUND CONTROLS + AUDIT TRAIL · Delivered orders close their lagging shipment rows (Linda healed + 35 prod rows + systematic). Refund button hides once nothing's refundable; popup shows available balance + custom $ amount (server-capped) + second confirmation. Audit notes name the human on every cancel/refund (Chris). Zoom-recap game plan delivered. 94/94.
2026-07-09 — v0.28.0 THREAD RAIL = FULL PROFILE + MEMBER LOCKDOWN + AUTO-ASSIGN + CONTACT HEADLINE · Thread sidebar renders the full profile inline (Recharge editor, orders, delivery — everything). Contact headline on profiles: last contact date/channel + whose court the ball is in (Dominick verified). Members see ONLY My Flippers + My-profile (name/email/pw) + a real Log out button. Unassigned tile += ownerless cancellations/address changes; ⚡ Auto-assign round-robins 40/click across regular members in Gorgias. Remove member ✕ redistributes their book equally. Deleted-in-Gorgias tickets self-heal to trashed (Trustpilot ghost gone). 94/94.
2026-07-09 — v0.27.0 LOCAL CSV LOAD (117k IDS) + CUTOVER OCT 31 ’25 + LAST-MESSAGE DATES · Flexport report CSV loaded LOCALLY into D1 (15 SQL chunks, ~3 min vs hours of API walking) — 117,690 orders stamped; true warehouse cutover = 10-31-25; March orders read Flexport. Tile dates = last message either side (new last_message_at, stamped from real messages — slim view payloads carry no datetimes); Dominick verified 06-19-26; enrich walk grinding the open set. Dates = MM-DD-YY numbers only. Dash cache 60s. Variant walk resumed. 94/94.
2026-07-09 — v0.26.0 STICKY HEADER + HOUSE DATES + SPEED 3 + TRUE TICKET DATES · Header sticks while scrolling. Dates read "Jul 9, 2026" everywhere (17 sites); chart labels angled w/ year; line graph default. Tiles show when the CUSTOMER last spoke (last_customer_at — updated_at lied on Dominick's ticket; 160 open tickets restamped). Speed: dash tile counts parallel + 30s cache (~0.8s warm), insights 212ms, email search 107ms (was 1.7s), order# rides its index, products cached 10min (167ms warm), profile orders capped 300, 3 new indexes. Year-long id ingest now stamping ~400/run at 54k/119k → March orders flip to Flexport as it lands. 94/94.
2026-07-09 — v0.25.0 TICKET DEDUPE + PASSWORD LOGIN + GREEN-LIGHT REMEDIES + HOUSEHOLD · 27 duplicate ticket pairs purged + UNIQUE index ends the webhook/sweep race. Password login for agents w/o Gorgias API keys (first login on a provisioned email sets it); master-admin link retired. ✅ REMEDIED green light: recent happy close (their own words) badges the profile + at-risk card; v2 = Gorgias CSAT + call/text sentiment. Household sharing field feeds supply math. Era = Flexport id OR data-learned cutover. Linda's chip reads "2 separate days". Chart defaults to line w/ angled dates. Pain points ×2 columns. 94/94.
2026-07-09 — v0.24.0 SKIP-BURST FIX + DELIVERY EVERYWHERE + RIVO CONTROLS + REAL ROSTER · "20× skipped" history was one customer mashing skip — bursts collapse to ×N rows and all skip metrics count distinct days. Delivery: line-graph toggle + years + ShipBob vs Flexport era split (id backfill extended to a full year); per-order analysis on the order sheet; 2 new Insights reports incl. subscribers vs one-timers. Edit-sub popup: portal-centered, retired variants hidden, live prices shown, real current values, Delete far left. Rivo grant/deduct from profile + at-risk card. Team tab = real roster w/ admin ✎ editing. Pain points truly 10. 94/94.
2026-07-09 — v0.23.0 DELIVERY EXPERIENCE + REFUND/CANCEL POPUPS + TEAM PERFORMANCE · Delivery experience on every profile: stats + transit-time chart + per-carrier + issues view. Refund popup shows full scope before money moves (preloaded full, item steppers, shipping toggle; Shopify does the math). Cancel popup — and cancel now fully refunds in the same motion. Team performance in Insights: per-agent closes/speed/backlog/saves/themes, coral vs team average (live: 2.5d vs 39.1d avg-close spread). Tab-switch tile bug fixed; tasks poll near-real-time; pain points = company-wide top 10; "My Hit List". 94/94.
2026-07-09 — v0.22.0 FALSE-RETURN HOTFIX + SUB EVERYTHING-EDITOR · Bug: Flexport ignores unknown query filters → every sheet adopted the SAME return (Martin's) onto 4 wrong orders — labels/notes/tasks all cleaned; matching now proves fulfillmentOrderId equals the order's Flexport id + no return adopts twice; Martin's order adopted correctly (verified). Everything-editor: Edit ⚙ on every sub row (profile + condensed at-risk card) — price, swap, qty, frequency, date, skip, cancel, reactivate, delete. Bulk changes across checked subs. Swap catalog = Recharge-plan products only (32 live). 94/94.
2026-07-09 — v0.21.0 SUBSCRIPTION SHEET + ADD/SWAP + REACTIVATE ROOT CAUSE · Click any subscription → full sheet (live Recharge price/qty/next-charge, history, all actions) with Swap product from the Shopify-fed catalog (dates/discounts stay put; verified live). + Add subscription on every profile — variant + cadence + first charge, created straight in Recharge. Reactivate root cause: cancel wrote to the FIRST row matching the Recharge id — a duplicate absorbed it; flips now hit the exact row (regression test), prod's one dupe pair cleaned. 93/93.
2026-07-08 — v0.20.0 RETURN RADAR V2 + CANCEL-SAVE REALLY CANCELS + MERGE HEALING · Order sheets adopt portal-created returns (verified on #272782: found at RECEIVED_FOR_PROCESSING → "OK to refund" + task + note), status timeline mirrors Flexport, Refund ↩ button on return tasks. Cancel-save outcome=cancelled now cancels the Recharge subs (was log-only — kept charging, no Reactivate). Stuck shipments stop flagging when a later order delivered / $0 reship sent / order cancelled. Email-twin auto-merge at ingest + phone fallback. VIP claim leaves the tab instantly; "✖ Remove — gone for good" (vip_excluded). Red subs cancelled badge (profile + VIP rows). Boot payload 110KB→35KB. 91/91.
2026-07-08 — v0.19.0 PROFILE SPEED + HEAT V2 + RETURN RADAR + REORDER FIT · Profiles open in ~¼s (were seconds: double bundle build + serial queries + a full-table LTV sort per open — all fixed). Heat v2: rage language in mined bodies (quote shown as "🔥 In their own words") or 2+ open negatives, ≤60d fresh — live: 2 heat cases, zero cross-tab overlap. Return radar: Return.* webhooks + cron poll → "📦 Back at warehouse — OK to refund" + refund task to the book owner. Reorder fit: consumption-based cadence reco on at-risk card + profile (never tightens for skippers). Thread → Full profile navigates immediately; Flexport links get /detail. Fx id backfill DONE: 28,902 stamped.90/90.
2026-07-08 — v0.18.1 INSIGHTS REPORT BUILDER · Eight canned reports over every source (orders/revenue w/ bags, skips, saves by member, pain themes, top products, LTV cohorts, shipping issues, VIP coverage), date range + day/week/month bucketing, table + CSV export. Verified live (July: 3,303 orders / $283,787). Duration bug fixed (formatter takes days, got ms). Products removable on /products (archive, history intact). 88/88.
2026-07-08 — v0.18.0 VARIANTS = BUNDLES (bags-aware supply) · The "ran out X days ago" root fix: variant catalog synced (Starter=1 / Double Flip=2 / Weight Shredder=3 bags — verified live, editable on /products), orders capture variant + units at ingest, rings count bags not line items. History walk running overnight (~274k orders). Remaining: consumption stats w/ seasonal filter, sub sheet add/swap, Insights reports. 87/87.
2026-07-08 — v0.17.1 VIP SEED + IMAGES + ROLE GATING + PERF 2 · VIPs seeded (chardo 148, jeannye 569). Dropdown shows the viewed account's REAL dashboard (members: no VIP board). Images fixed — inline body_html photos (Eric W. verified) render in threads + claims exports. DUE badges + blurb admin-only. Header never overlaps. Body walk running (~6h) → pain mining over every conversation. React self-hosted + immutable caching (shell 119ms). Rule: actions on Shopify/Recharge/Flexport only — Gorgias stays swappable. Next: variants epic. 86/86.
2026-07-08 — v0.16.1 COMMS EXPORT + VIP RETENTION · Export conversation (print-ready → PDF for claims) on threads, tile rows, profile tickets. VIP retention board = admin default on My Flippers: member's $1k+ due-for-contact VIPs, longest-quiet first; logging drops them back into the pool. Book list retired (members pick a tile). VIPs tab = unassigned $1k+ only. Chardo + Jeannye now admins. Pain points bucketed into themes. 86/86.
2026-07-08 — v0.16.0 WAVE 15 CORE · Precompiled UI — no more in-browser compiler; near-instant loads on the same Worker. Tab URLs (#tab=…) — back button walks everything like pages. My Flippers inline: tiles/tasks open in the bottom section (own URLs); book = default. Member hero: top-5 pain points for their book, saves started, saves confirmed (paid-reorder only), save rate w/ day/week/month/all picker. Flexport id backfill ingesting (Deliverr-named report columns). Remaining: full-profile thread rail + deep scoring pass. 86/86.
2026-07-08 — v0.15.0 WAVES 13+14 · Cancel-save only on a real open cancel ticket (skips don't count). Unread = Gorgias's **NEW MESSAGE** tag (222 → matches their 8). Shipping tiles new-only; ship-waiting rides the member's Waiting tile w/ filter chips. Cancel/address buttons hidden once fulfillment starts. ⇄ Merge customer (admin) fixes Gorgias-merged dupes. Flexport Order.* webhooks registered — claim links self-activate as orders move. Returns wizard: pick items/qty → Flexport buys the label → emailed to the customer via Gorgias as the member. 86/86.
2026-07-08 — v0.14.1 CLAIMS + RETURNS + CANCEL-INTENT · Returns from the app: Start return 📦 → Flexport buys the label (built from our SKUs + address; no order-id lookup needed); label link copy-ready for Gorgias; auto-noted. Claims: portal deep link when the Flexport id resolves; vendor bug (external-id lookup unreachable for "#…" ids) → fallback File claim (search) ↗ with order # auto-copied. Fulfillment-order scopes added (also unblocked live cancel gating). Cancel-intent: Run cancel-save only for customers honestly cancelling (UI + server); saves read PENDING PAID REORDER → SAVE CONFIRMED ✓ with the member's name. 86/86.
2026-07-08 — v0.14.0 "FLIPPER" + FIVE RULES · App renamed FLIPPER (store brand stays on receipts). OPP fully removed. Fraudsters never VIPs (admin ⚑ flag + fraud-tag heuristic). A save = a PAID reorder — $0 reships don't count; unconfirmed saves read "pending". Unassigned rules: bot/agent-initiated tickets excluded (from_agent); unassigned = new + unclaimed + customer in nobody's book; book-owned customers' new tickets auto-route to the owner's hit list. Flexport claims research: no claims API exists (latest 2026-02) — support-portal only, 90-day window; outbound LOST_DAMAGED status auto-initiates a claim → watch via webhooks + pre-filled Gorgias claim emails is the path. 87/87.
2026-07-08 — v0.13.3 GORGIAS WEBHOOKS LIVE — TILES REAL-TIME · Tyler registered the HTTP integration; live payload = {ticket_id} only → handler accepts full ticket / envelope / id-only with API fetch-back; junk payloads never mint rows (first delivery's ghost row deleted). Verified live end-to-end (idempotent). Ticket changes hit the tiles in seconds; the 30-min sweep stays as safety net + bot correction. 85/85.
2026-07-07 — v0.13.2 RETURNS PIPELINE + BOT-AWARE CONTACT + JUNK RECONCILE · Returns are a pipeline: `Awaiting Return` → the ticket lives ONLY in Active Returns until CLOSED. Gorgias Bot ≠ contact: tiles verify agent-spoke-last tickets against real messages — 8,495 verified, 674 flipped back to needs-agent (bot-only "answers"). Junk reconciled: true open set ~12.8k stamped; 740 ghost rows (spam/trash invisible in Gorgias views) excluded — unassigned 30 → 5. Thread route hardened (customer panel survives Gorgias failure). Flagged next: WAITING tiles bloated by the never-closed backlog (2,000+ per senior agent) — age/auto-stale policy to discuss. 84/84.
2026-07-07 — v0.13.1 TILES V2: OWN TICKET STATE MACHINE + SPLIT VIEW · Tiles rebuilt on our own states (NEW / NEEDS AGENT / WAITING — agent-spoke-last never shows in urgent/semi-urgent): shipping + address changes + cancellations now team-wide; hit list / unread / waiting / returns follow the Gorgias assignee (member unread = their Gorgias unread; chardo's call-list tag counts). Shipping split into new vs waiting-on-customer ×2; customer replies pipe into unread, agent replies back to waiting. New Waiting for response member tile. New-channel dupes → the agent's unread, never unassigned. Junk exclusion fixes "20 in Gorgias, 30 in app". Sticky ownership: cron auto-assigns new customers, never moves existing. Thread split view: ★ Profile toggle — customer panel right (desktop) / top (mobile), remembered. Bubbles: no tag chips; red NEW TICKET, orange AWAITING REPLY. 83/83.
2026-07-07 — v0.13.0 FLIPPERS TILE DASHBOARD · My Flippers is a tile dashboard: Tasks tile → full task list on its own URL; Active returns; admin-only Unassigned. Messages tiles mirror the live Gorgias views (tag vocabulary probed from their real view filters): Shipping issues (Lost & damaged · Wrong item), Urgent (Address changes · Cancellations · admin-only Hit list), Semi-urgent admin-only (Unread responses). Visibility per Tyler: members see shipping + address changes + cancellations + returns scoped to their own book; the admin dropdown drives tasks + messages + book together. Tiles open full-screen ticket lists on own URLs → rows open the full-screen thread. Plumbing: tickets.tags/updated_at/awaiting_reply/channel captured; open tickets swept via the system "All" view (items paginate by meta.next_items — not next_cursor); cron keeps tiles ≤30 min fresh. Verified live: Alexis 23 active returns · 5 unread · admins see 12 unassigned. 83/83 tests.
2026-07-07 — v0.12.3 FULL-BOOK RESYNC + MEMORY-SAFE CACHE REBUILD · Gorgias walk completed at 47,806 tickets; the ownership resync was rebuilt batched (per-owner groups, ≤90-id IN chunks, 300-statement budget, remaining flag — the row-at-a-time version was 40× over the Workers subrequest cap) and landed 15,045 reassignments in one run. Full cache rebuild hit the Workers 128MB memory cap at 8 member books (and starved live webhooks) → computeAndCacheQueues gained a books option: ?fresh=1 skips books, books rebuild solo via /api/my-flippers?fresh=1, and the cron rotates 2 books per run (every book fresh within ~2h). Final books: cassyjewel 7,654 · starrsmith 5,836 · alynne 5,330 · lindsey 5,102 · alexis 1,975 · support 1,755 · jeannye 567 · chardo 146 — all three missing teammates now stocked. Address + order-number backfill running overnight. 82/82 tests.
2026-07-07 — v0.12.2 FULFILLMENT-AWARE CANCELS + RESHIP VERIFY + TASK DELETE · Cancel reads Shopify fulfillment orders: fulfilled → blocked (409, "use a refund"); submitted to Flexport → cancellation_request rides through Shopify to Flexport then cancels; untouched → direct cancel — the two-system dance is one button. Reship gains a verify-address step before send. Order history rows lead with the order # ("syncing…" until backfill fills old ones). Admins can delete tasks (✕). Bot-assignee guard landed pre-resync. Flippers tile-dashboard epic filed (Tasks tile → own URL; Messages tiles mirroring the Gorgias views hierarchy; Active Returns tile). 82/82 tests.
2026-07-07 — v0.12.1 TASKS IN MY FLIPPERS + ORDER NUMBERS + INVOICE RECEIPT · Tasks panel tops each member's board (due dates, overdue red, customer link, Done ✓ → counts as contact). Order numbers now stored + shown (#gold) in reship picker + order history; history fills via the queued backfill. Receipt v2: invoice layout — brand header, ship-to/bill-to columns, SKU table, subtotal/discounts/shipping/tax/total (Tyler's example didn't attach; matched OPP's standard structure). Credit = dollars-only UI. 81/81 tests.
2026-07-07 — v0.12.0 REAL QUICK ACTIONS + RECEIPT PIVOT · OPP links unfixable (their internal ids; "ORDER NOT FOUND" ships as a 200 PDF — verify content, not codes) → order sheet now has Print receipt 🖨 (our own, from live order data) + Open in Shopify ↗. Quick actions are REAL: Reship = $0 duplicate via draft order → ships through Flexport normally; Apply credit = $ or %, Recharge account credit OR partial refund on latest order, with admin-set team ceiling (members capped, admins uncapped); Win-back = template picker (Klaviyo pipeline later); Create task (was a placeholder) = assigned follow-up touchpoint w/ teammate + due date. Notes removable (✕, DELETE /api/notes/:id). saveRiskConfig now merges (settings + risk keys can't wipe each other). Sub sheet + add/swap subs → variants epic. 80/80 tests.
2026-07-07 — v0.11.0 ORDER SHEET + SUBSCRIPTION MANAGEMENT IN-APP · Orders in profiles are clickable → full-screen live Shopify order with Cancel / Full refund (calculate→create) / Change shipping address / Receipt PDF via Order Printer Pro (URL rule solved: raw Shopify id works — verified 200 PDF live). Subscriptions card on every profile: all subs + in-app Recharge actions — Skip next charge, Change date, Cancel (reason), Reactivate (needs write_subscriptions/write_charges scopes). Points policy: 1pt = 1¢, $25 redemption cap — lever now fires; prod balances repaired. Durations humanized ("1 year, 2 months, 3 days"). 79/79 tests.
2026-07-07 — v0.10.1 ADDRESSES · Never stored before; now sourced from orders — "all their addresses" + most recent order = default for free. Capture on every Shopify/Recharge order (compact JSON, both upsert branches); latest formatted address denormalized onto the customer for search-by-address (🔍 placeholder updated). Profile card: SHIPPING + BILLING side-by-side, "billing = shipping ✓" chip when identical, "from their latest order" date, collapsible other-addresses history. Backfill endpoint walks Shopify orders newest-first (page_info pagination) so current addresses land first; resumable; loop queued behind the running Gorgias walk. 78/78 tests.
2026-07-07 — v0.10.0 OWNER BADGES + PAGING + SEARCH + FULL-SCREEN THREADS · ★ owner chip on every customer name (queue, VIPs, radar, detail, search). Contact truth from Gorgias: moniker + 105-day cadence use max(logged outreach, latest Gorgias convo) — no more false "never contacted". Threads are full-screen hash-routed pages (back or ✕ closes) — the base for more in-app Gorgias tooling. Paging: 500-item cache blocks, top-1,000 per list ('all' queue 5,000 deep), ?offset&limit APIs, UI shows 20 at a time w/ Show-more; VIPs continue past cache via live SQL pages. Master 🔍 search (name/email/phone → profile). Partial-seed root cause fixed (first walk covered 14.4k of 34k tickets) — full re-walk running, ?resync=1 trues every book at completion (missing accounts incl. alexiswoodring98/jeannye/lindsey.fein auto-provision). 77/77 tests.
2026-07-07 — v0.9.3 BOOKS SEEDED + CACHED · Archive walk done → seed ran: 7,435 customers assigned (cassy 3,410 · starrsmith 2,866 · alynne 729 · support 430; 3 accounts auto-provisioned). Real book size exposed two scale bugs, both fixed: D1's 100-bound-param cap (IN clauses now ≤90-id chunks) and 15s live book loads (≈45 serialized D1 round trips) → books precomputed into queue_cache per member, top-200 by LTV, 94–344ms cached. Temp ops account deleted. 76/76 tests.
2026-07-07 — v0.9.2 ASSIGNEE BADGES + GORGIAS-SEEDED BOOKS · Detail payload never carried assigned_to → admins overwrote each other blind; now a ★ owner badge on every profile (all viewers) + real value in the admin dropdown (assignment already admin-only server-side). Books seed from Gorgias ownership: tickets capture assignee_email; seed endpoint + "Seed from Gorgias" button assigns unassigned customers to their latest conversation's owner, auto-provisioning keyless member accounts; archive re-walk running (~3h, seeds at completion). Calling/texting: GV has no API — shipped native tel:/sms: links (each member's own phone/GV app); real in-app dialing = Telnyx/Twilio (~$10–30/mo), or Gorgias SMS channel if enabled. 75/75 tests.
2026-07-07 — v0.9.1 WEBHOOKS VERIFIED + KEY-FREE LOGIN · Signature saga solved — Tyler's credentials were right all along: Recharge's "HMAC" header is actually plain sha256(client_secret + body), not a keyed HMAC; our correct-HMAC code passed its own tests and rejected every real webhook. Both constructions now accepted — first live `ok` was a real skip, captured in seconds. Access model: APP_KEY now guards ONLY first-admin bootstrap; the login page is public, a login wall fronts the app, and all data routes require a session in production. Member API-key field: visible mono text (password-type autofill mangled pastes). 74/74 tests.
2026-07-07 — v0.9.0 TEAM LOGINS + MY FLIPPERS · Sign in with Gorgias: members use their Gorgias email + personal API key (verified live; auto-provisioned with their Gorgias identity) — in-app replies send from Gorgias as them. Master admins (password, one-time bootstrap) create users + act as anyone. Assignment (admin dropdown on every profile) feeds the My Flippers tab: the member's book scored by the risk engine, with the outreach cadence board — DUE at 105+ days since a confirmed conversation, Log call/text ✓ rests them until next cycle; admin team picker. Same DUE + log buttons on VIPs. New: app_users + sessions tables (30-day revocable cookie). Verified in-browser end-to-end. 73/73 tests. OPEN: Recharge webhook signature still mismatching after exhausting every implementation cause — needs Tyler to confirm the exact token page the secret came from; sync carries all data meanwhile.
2026-07-06 — v0.8.3 GORGIAS ACTIONS IN-APP + TENURE FIX · Reply to Gorgias threads from the app (real email out through Gorgias; composer in the thread modal) + close/reopen tickets in-app (mirrored to Gorgias first). "Only CLOSED = resolved" enforced — reopened tickets lose stale resolved stamps. VIP tenure fixed: first_order_at was write-once from the 60-day-window era → now true MIN, 11,990 rows repaired (Barry: 2.3 years, verified). Phone (tap-to-call) + email on profiles. Webhook rejects diagnosed: signature mismatch — Tyler to re-copy the API Client Secret from the same Recharge token as the saved key. 72/72 tests.
2026-07-06 — SKIP HISTORY + RIVO PASSES COMPLETE · Full skip archive ingested & attributed: 18,496 events · 6,196 skippers · 3,025 repeat skippers (2+ in 90d); subs top-200 all high-tier. Rivo: top ~10k LTV verified per-id, 5,728 live balances. Webhooks + cron maintain both from here.
2026-07-06 — v0.8.2 THE GREAT PURGE (Tyler-approved) · Root leak found: guest/POS orders with no customer identity minted a fresh husk row on every sync pass — 227k orphans accumulated (far beyond the skip bug's ~9.5k). Root fixed (no-identifier payloads never mint customers; customer-less orders skipped). Executed: 12,643 skip events re-pointed to their subscriptions' owners; 187,387 orphans deleted; 30,939 stale Rivo ids cleared; customers ~380k → 153,567 real rows. Verified after: categories 200–300ms, subs top 100, VIPs 72/100 with points, profiles intact. 70/70 tests.
2026-07-06 — v0.8.1 THREE LIVE FIXES: webhooks one-click · subs tab alive · Rivo points real · (1) Recharge webhooks register themselves now — "Register webhooks" button on /connections (server uses the stored token; all 5 topics registered live; curl instructions obsolete). (2) Subs tab: skip crawl flipped newest-first; 2021-11 charges nest the customer — the flat read minted ~9.5k ghost customers holding every skip event (zero signals). Skips now attribute to the subscription's owner (9,498/9,500 resolve) and INSERT OR REPLACE re-crawl heals old rows. Subs category 0 → 100+, score-100 customers stacking stalled+depleted+skipped+repeat-skipper; ~9k skippers in 90 days. (3) Rivo: list filters are silent no-ops (the email "lookup" smeared one member's zero balance over 37.5k rows) and pages cap at ~250 — but Rivo's member id IS the Shopify customer id → per-id lookups. Two-phase sync (bulk 25k + targeted top-10k LTV). Verified: Weinstein 550 pts exact; 1,841 real holders; 72 of top-100 VIPs show points. (4) VIPs cached: 1.8s → 230ms. Pending Tyler's OK: purge ~9.5k ghost rows + stale rivo ids. 70/70 tests.
2026-07-06 — v0.8.0 SKIP TRACKING — the real pause metric · Tyler: customers don't pause, they skip. Mechanism found: Recharge keeps the sub ACTIVE and marks the CHARGE skipped — subscription-status watching could never see it. Upside: skipped charges are queryable history (/charges?status=skipped) → the pattern backfills retroactively. Shipped: skipped charges ingest as subscription_events (deterministic se_rcch_<id> → idempotent across re-runs + duplicate webhooks); one-time full-history backfill phase in the Recharge sync (cursor-resumable) then incremental; charge/updated webhook handled (checklist: register the topic — else skips ride the 30-min cron); risk engine: renewal_skipped now fires from a skip event ≤60d on a non-cancelled sub, repeat_pauser reworded skips-first (≥2 in 90d unchanged). Live run 1: 500 skips from the first 2 pages; loop grinding (~500/3min). 69/69 tests.
2026-07-06 — v0.7.1 SCALE FIX: 503 → instant dashboard · Tyler's "live queue 503" = Worker memory blown by bulk-loading all 274k+ orders per request. Fix 1: aggregate-first scoring — per-customer orderFacts (COUNT + last anchor via GROUP BY), full rows only for the trailing 150 days, shipments joined to recent orders; risk signals consume aggregates with full-row fallback. Fix 2: even fixed, live-scoring ~90k buyers ≈ 13s/request → new queue_cache (top-200 per category), refreshed by the 30-min cron; /api/queue serves it instantly (computed_at exposed, ?fresh=1 rescores, cache-miss self-heals). Verified live: 74–363ms (was 13–20s), top risk Owen Self 83. Subs category legitimately empty (20,911 active / 48,696 cancelled / zero paused — pattern history accrues from live events). MILESTONE: Shopify (~274k orders, to Aug 2023) + Recharge (~45.5k identities) backfills DONE. Follow-up: Gorgias incremental marker. 68/68 tests.
2026-07-05 — v0.7.0 FULL CUSTOMER PROFILES · Any customer, anywhere → URL-addressable profile (#customer=id, back-button friendly): action panel + complete order history + touchpoints + notes + ticket threads; self-contained fetch (works beyond the loaded queue — modals now carry their customer in state). Entries: queue "Full profile →", clickable VIP rows, radar Open. Verified desktop+mobile. Session milestones: Gorgias backfill COMPLETE (~34k tickets); Shopify ~274k orders; Recharge identities DONE (~45.5k) — VIP list transformed (Barry Marburger $5,303 · 22 orders · active sub).
2026-07-04 — v0.6.0 MOBILE BUILDOUT · Responsive across phone/tablet/desktop (verified 375/768/1280): 900px+640px breakpoints, coarse-pointer targets ≥42px, 16px inputs (iOS zoom guard), safe-areas. Queue on mobile = list → full-screen detail sheet (tap customer, sticky back bar; shared Detail, no duplication). Grids stack (stats 2×2, radar/VIP rows, auto-fit insights/team), scrollable tabs, full-screen modals ≤640. Lesson: matchMedia/resize events unreliable under emulation — read matchMedia live per render, listeners only re-render. Backfill at deploy: ~74k orders, 40k identities unified, 24k+ tickets; concurrent loops → D1 contention → now a sequential grand pipeline.
2026-07-04 — v0.5.3 Recharge identity unification · Zero VIP sub badges after the active-subs phase → diagnosis: Recharge sub payloads carry no email, so subs landed on bare stub customers invisible next to Shopify identities. mergeRechargeCustomer + a customers-first identity phase in the Recharge sync unify them retroactively (children moved, stubs deleted, rollups recomputed). Subs-category=0 on active-only data is correct (healthy subs = no risk signals). History crawl settled at pages=2 (500 orders/run); retry covers D1 "connection lost" blips. 67/67. Backfills grinding: order history Aug 2023→, Recharge identity+history, Gorgias 24k+ tickets.
2026-07-03 — v0.5.2 FULL ORDER HISTORY UNLOCKED · Correction: the store sells since Aug 2023 — "spring 2026" was Shopify's silent 60-day order window for apps without read_all_orders (oldest ingested order sat exactly 60 days back). Tyler granted the scope + released + approved the store installation update (all three needed). Verified: orders visible to 2023-08-07. Orders cursor reset, Worker redeployed (token cache), full-history crawl running (likely six figures of orders). Checklist hardened so this scope can't be missed again. Recharge active-first: 1,500+ live subs and counting.
2026-07-03 — v0.5.1 deep-views + VIP v2 + Recharge cursor fix · Gorgias thread click-through: ticket snippets open the full live conversation in a chat modal (/api/tickets/:id/thread) — verified on Roxanne's real refund ticket. VIP v2: best customers = tenure + (live sub OR repeat orders), LTV-ranked, 60s auto-refresh; store is YOUNG (history starts spring 2026) so tenure default dropped 180→30d — live top: Jason Jimmy $3,634/14 orders, never contacted. Recharge cursor pagination (page numbers 422 past ~page 98 with active subs beyond the wall) + active-first priority phase (status=active crawls before history). 66/66.
2026-07-03 — v0.5.0 Retention 2.0 · Tyler's batch shipped: At-risk category drill-down (server-side ?category= — client filtering couldn't surface low-weight pools) · repeat_pauser signal (22) from new subscription_events transitions (≥2 pause/skips in 90d) · ring buffer: dosing counts from delivery + 3 days · Gorgias cross-ref: replacement-type tickets annotate shipping/supply signals + ticket panel in Detail · lapsed_buyer (12) win-back pool · VIPs tab + /api/vips (tenure-ranked, last-touch guarded, Reach out / Log check-in). Flexport closed: no order-list endpoint exists on this API version (single-order GET + webhooks; tracking already flows via Shopify) — the scope error was a ghost. Milestone: first HIGH-tier customers (Roxanne Saldana 83 — three signals stacking). VIPs/subs category populate as the Recharge crawl (21.5k+ so far, oldest-first) reaches active subs. 65/65 tests.
2026-07-03 — v0.4.0 THE DASHBOARD IS REAL · The branded UI's mock data ("fake customers") is gone — full rewrite onto the live API: queue top-50, lazy detail (real orders + shipments timeline; server adds shipments/email/phone to payloads), save plays from the API (slug keys end-to-end — M1–M5 gone by construction), saves/notes/touchpoints POST live, Insights + save-rates live, radar from live signals, call panel labeled demo. Incident: PS 5.1 UTF-8 round-trip mojibake — caught + fixed via clean rewrite (rule: never Get/Set-Content UTF-8 source in PS 5.1). Verified locally (Brad Lloyd's real stalled JITSU shipment renders) + live. Flexport scope failures now FAIL the card with instructions (Tyler's new token still needs ORDER read).
2026-07-03 — v0.3.2 all five services connected · Rivo/Gorgias/Flexport failures were ALL adapter-side: Rivo = raw key, no Bearer, /merchant_api/v1 + User-Agent required from Workers egress (now on all adapters) · Gorgias = domain input normalization (https:// prefix broke URL building) + probe /tickets not admin-only /account · Flexport = Seller Portal tokens hit the Logistics API (logistics-api.flexport.com/logistics/api/2024-07), and unscoped routes 404 silently — probe now names missing scopes. Rivo sync cursor bug fixed (was stuck on first 200); Gorgias sync chunked. Scoreboard: Shopify ✓ Recharge ✓ Rivo ✓ Gorgias ✓; Flexport needs order+parcel read scopes added by Tyler. 59/59, deploys green-gated.
2026-07-03 — v0.3.1 DEPLOYED + Recharge live · Tyler authorized wrangler + enabled Workers Paid → D1 created, schema + 196,787 rows imported (23s), subdomain flipmylife.workers.dev (Tyler's choice) → https://flip-cms.flipmylife.workers.dev live with 30-min cron. Access gate: APP_KEY (?key= once → 30-day cookie; key in ACCESS_KEY.txt; webhooks exempt). Verified: 401 keyless / 200 with key / data from D1. Recharge connected — 403 was my /store test probe (unscoped endpoint), moved to /subscriptions; sync rebuilt chunked + incremental for the Workers ~1000-subrequest cap (page cursors, updated_at_min passes, ?pages= override). Deploys now: npm run deploy — gate on green tests. 59/59.
2026-07-03 — v0.3.0 Cloudflare Workers + D1 port · No PC-as-server: full async port to Cloudflare's cloud. One db facade, two engines (node:sqlite local/tests, D1 prod); api.js rewritten fetch-native (same handler on Workers + locally via a thin node adapter); worker.js entry with ASSETS statics + 30-min cron auto-sync (closes that P1); wrangler.jsonc; deploy/export-data.js → schema.sql + data.sql (196,785 rows, 30.4MB, incl. cursors — deployed instance continues incrementally). 58/58 tests post-port; local verified on full data (~500ms queue). Pending: Tyler's wrangler OAuth → D1 create + import + deploy + Access gate. Workers Paid ($5/mo) recommended (free D1 caps 100k row-writes/day; import is ~197k). New 12th product needs dose facts.
2026-07-03 — v0.2.4/0.2.5 full backfill + scale · Dose facts entered for all 11 products → 16,382 live rings, real churn patterns in the queue. Sync cursors (per-page persistence, remaining flag, 12-pages/run after a 40-page run outlived undici's 5-min timeout, 429 retry). Backfill: orders caught up ~25.9k (cursor even caught single orders placed mid-sync); customers passed 110k (mostly marketing contacts) → forced set-based queue scoring (6 bulk queries, identical tested logic, buyers-only filter). Zero high-tier on Shopify-only data is expected — the third stacked signal arrives with Recharge/Gorgias. 58/58 tests.
2026-07-03 — v0.2.3 demo purge + dose-facts screen · Purged demo customers c1–c6 + webhook artifacts + 4 orphaned demo products (team/routing config kept). Real tenant: 9,884 customers · 11 FLIP 7 products · 5,000 orders (both crawls capped — continuation pending). Built /products dose-facts screen: usage-sorted list (orders/customers counts, ring-ready chips), inline servings + daily dose editing, GET/PUT /api/products validated; end-to-end test proves a ring lights up once facts are set. Verified live on the real catalog (top: FLIP 7 Super Shake, 2,311 orders). 56/56 tests. Tyler's next manual step: fill in the dose facts.
2026-07-03 — v0.2.2 first real store + live-data hardening · Tyler connected flipmylifenow.myshopify.com (Dev Dashboard client-credentials, status Connected); first sync pulled 5,000 customers + 5,000 orders (crawl cap — continuation on the board). Real data exposed + fixed: null-ring crash on dose-less auto-created products (queue 400 → guarded, regression-tested) · O(n²) top-decile recomputation → precomputed once · 11 new indexes for per-customer scoring + upsert lookups · /api/queue?limit= (default 200). Queue on real data: 200 OK ~1.1s with real stalled-shipment signals. Connections secret-field UX redo (secrets never echoed; "Saved — ends in xxxx" + × to clear). +1 test → 55/55. Demo customers still mixed with real data — purge pending Tyler's call.
2026-07-03 — v0.2.1 Shopify Dev Dashboard auth · Shopify retired new legacy custom apps on 2026-01-01 (Tyler hit this live). Connector reworked: shopifyToken() = OAuth client-credentials grant (Client ID + secret → 24h token, cached, auto-refreshed — user never touches tokens); legacy shpat_ still accepted but a complete client pair takes precedence; webhook verify accepts Notifications-page secret OR app client secret; blanking a Connections field now clears it; checklist rewritten (App URL fields = placeholders for server-to-server). Constraint: app + store must share a Shopify organization. +3 tests → 54/54.
2026-07-03 — v0.2.0 integration layer (Claude Code / Tyler) · Tyler approved the single-server + API-first plan and asked to connect Shopify / Recharge / Rivo / Gorgias / Flexport. Built: persistent SQLite (idempotent seed, in-place migrations) · single server (UI + API + webhooks) · credential store with masked secrets + settings API · signature-verified webhook receivers for all five services + audit log · backfill syncs · idempotent ingestion layer (lib/upserts.js: external-id matching, cross-platform customer unification, LTV recompute, delivered→ring-anchor) · new shopify.js + gorgias.js adapters · live Connections screen · fixes B1, B3, B4, B5, B7, B11 (+B6 partial) · 16 new tests → 51/51. Verified live: signed webhook → new customer in queue → survived restart; forged signature → 401. Next: real credentials + public webhook URL, then wire the branded UI.
2026-07-03 — v0.1.0 audit (Claude Code / Tyler) · Received zip + build conversation from the Chardizy builder. Extracted to F:\Claude Code\Flip CMS\, installed Node 24.18.0, verified 35/35 tests + live API + branded UI + cancel-save flip flow. Recovered full build history from the Claude share link. Created this breakdown (MD + HTML). No app code modified. Findings logged B1–B12, M1–M5; TODO board seeded.
Pre-handoff (build convo) · v0.1.0 built in one Claude session: gap analysis → clickable prototype (dashboard.jsx) → call/text scheduling + routing + tagged notes + insights → cancel-save scripts + data-model spec → zero-dep Node backend + tests → "Flip My Life" branded UI → Flexport/Recharge/Rivo/Twilio-Whisper adapters → Rivo panel + order timeline → fulfillment radar + price points-lever. 35 tests green at handoff.