FrameCraft Operations
Program dashboard
Re-verified 2026-07-24 by a 16-cluster, 91-item code audit against Anthony's FULL architect handoff (JOHN-HANDOFF + 301 map + personalization + size-spec + SBF content + CFS QA), with every DONE/PARTIAL claim adversarially re-checked (14 over-optimistic claims corrected). Headline: 38 DONE / 36 PARTIAL / 8 NOT-STARTED / 6 BLOCKED / 2 DEFERRED. The #1 non-negotiable — server-rendered BODY content for AI crawlers — is genuinely DONE on all three stores. Most 'PARTIAL' items are one-store-only or behind an off flag, not missing work. The launch-critical gaps: (P0-legal) the personalization consent gate exists as a proofread checkbox but is NOT bound to the order record (no timestamp/text captured as a line attribute) and the ToS 'Personalized and Custom-Text Items' clause is absent; (P0-SEO) the 301 priority tier is done but the long tail hard-404s — Rule 6 catch-all, Rule 4 moulding 1:1s, Rule 5 chains, ~11 category .html + ~17 CMS remaps, and .html/case normalization are all missing; (P0-commerce) the Shopify variant ID + high-priced placeholder still pending. Newly-surfaced, not previously tracked: GTM (GTM-WNC3937K) ships on ZERO flagship pages (CPF has no analytics; store-b only fires with an unset env; only CFS is wired); store-b omits Organization+WebSite JSON-LD site-wide; flagship+store-b frame PDPs have no 120-char meta-description floor (store-b ships 18 four-word taglines); store-a/store-b have NO loading.tsx skeletons at all; voice/brand lint fails on the stores (em-dashes, ~30-40 banned words, exclamations, an emoji); /samples OG image is 1.69MB; the welcome.custompictureframes.com preservation rule is documented NOWHERE in the repo. Two-source reviews avoided the conflation trap (seller single-sourced, product scope distinct) but the live Reviews.io widgets aren't wired. Customer accounts are a full real vertical slice, live only on CFS; CPF/SBF need the same-site /bff auth proxy + flag flip for 'one account across all brands'. RB2B / Clarity / Klaviyo / 3D-spinner / loyalty-vendor are all correctly deferred to post-launch. Full report: docs/reference/LAUNCH_READINESS_AUDIT_2026-07-24.md; reusable rules for future storefronts: docs/reference/migration-playbook/.
Needs attention
top blocker & high-severity defectsMock checkout — no real commerce
/cart is built and the checkout handoff to the BFF works: an e2e intercepts the POST and asserts priceCents, the packing block and storeCode CPF. Two things remain before a REAL order can be placed: NEXT_PUBLIC_SHOPIFY_FRAME_VARIANT_ID must be set in the Vercel prod env (see variant-id-env), and a live test order has to land in Shopify Admin. Until the variant id is set, checkout falls back to a mock GID and Shopify rejects it, with no symptom in local dev.
Personalization consent gate not built
Pre-launch legal requirement on any permanent-text flow (nameplate/engraving/mat lettering). Mechanism unbuilt (FL-F05). Pairs with decision D5 (counsel wording).
Flagship points at an API host that does not exist
`apps/flagship/.env.local.example` (and the default in `src/lib/env.ts`) set `NEXT_PUBLIC_API_URL=https://dev-api.custompictureframes.com`. **That host does not resolve.** Neither does `api.custompictureframes.com`. Both fail DNS outright (curl: could not resolve host, 2026-07-13). So flagship's cart would not merely be blocked by CORS — it would never reach the BFF at all. The request dies on DNS. There is ONE BFF (`api.customframesizes.com`) serving all three brands: they share a single Shopify store (`cpf-headless`), and the per-brand routing rides on the `_fc_store_code` cart attribute, not on separate API hosts. Verified live: `api.customframesizes.com/api/health` -> 200, `/api/health/db` -> `{"ok":true}`. DECIDED (Ben, 2026-07-13): **one API, many storefronts.** framecraft-api at `api.customframesizes.com` is THE api service; the monorepo apps are storefronts that call it. No per-brand API host — the brand rides on `_fc_store_code`. FIXED in custom-frame-sizes#412. All three storefronts now default to `api.customframesizes.com`, and it was worse than filed on two counts: - store-b was equally broken: `api.shadowboxframes.com` and `dev-api.shadowboxframes.com` both return **525** (broken TLS). Only the customframesizes.com API hosts have ever been real. - The default never reached the browser anyway. `env.apiUrl` is dead config (nothing reads it); the cart pages read `process.env.NEXT_PUBLIC_API_URL || ""`, and `""` means SAME-ORIGIN — so with the var unset a storefront POSTed `/api/cart/fresh-checkout` to itself and 404'd. They now default to a named `FRAMECRAFT_API_ORIGIN`, verified baked into the client bundle. A unit test had been pinning the fictional `dev-api.custompictureframes.com` and passing the whole time. Replaced with a repo-wide guard across all three storefronts. TO CLOSE: set `NEXT_PUBLIC_API_URL=https://api.customframesizes.com` in each storefront's Vercel env (the code default now covers it, but make it explicit), and confirm a real checkout reaches the BFF.
CPF assets: the buckets are 86% populated — the CDN env vars are commented out
**CORRECTED 2026-07-13 — this issue was mostly wrong, twice.** Earlier versions said "2,416 files to upload", then "2,874". Both only ever looked at `apps/flagship/public/`. Nobody probed the actual bucket. When probed: | | count | status | |---|---|---| | `frames/` | 2,393 | **already on `assets-flagship.customframesizes.com`** ✓ | | `mats/` | 92 | **already on `shared-assets.customframesizes.com`** ✓ | | other, present | 39 | ✓ | | **other, MISSING** | **414** | images/ 161 · assets/ 110 · lifestyle/ 110 · comic/ 13 · sonogram/ 7 · specialty-hero/ 6 · glazing/ 2 · components/ 2 · magazine, puzzle, military 1 each | **2,524 of 2,938 (86%) are already uploaded.** They 404 today for ONE reason: `NEXT_PUBLIC_CDN_FLAGSHIP_URL` and `NEXT_PUBLIC_CDN_SHARED_URL` are **commented out** in `apps/flagship/.env.local.example`, so `getStoreCdnUrl()` returns null for flagship and every asset helper falls through to an empty local `/public`. So the single highest-value action on the CPF board is **uncommenting two env vars and setting them in Vercel** — that fixes 86% of the broken images with no upload at all. Then upload the remaining **414**. Bucket family settled empirically (D1 in the migration plan): use the custom domains `assets-flagship.customframesizes.com` + `shared-assets.customframesizes.com`. The `pub-*.r2.dev` origins are the same buckets by raw name; the two ids in `asset-urls.ts`'s comments return **401** (not public) and should be deleted. Of the 414: most exist in `original-cpf`; 87 need a RENAME (flagship invented tidy paths for files the original keeps flat in `attached_assets/`); and **6 exist nowhere and must be created** — `/assets/reviews/reviews-hero.png`, `/assets/specialty/photo-restoration-hero.png`, `/assets/visit/jersey_framing_central_nj_hero.png`, `/components/foam-board/{standard-white,standard-black,self-adhesive-white}.png`.
/api/proxy-image is an unauthenticated SSRF
The route validates only that the string parses as a `new URL()` — **no protocol check, no host allowlist** — then does a server-side `fetch(url)` and returns the full response body base64-encoded to the caller. That reaches cloud metadata (`http://169.254.169.254/...`) and any internal service, and hands the contents back to the attacker. No size limit either: `arrayBuffer()` on an arbitrary URL is a memory DoS. Unauthenticated, like all 9 store-a API routes. This is provably an oversight rather than a risk decision: the sibling route `objects/upload-from-url/route.ts` already implements `isAllowedSourceHostname` (replicate.delivery only) and enforces `https:`. The pattern was known and not applied here. MUST be fixed BEFORE this route is copied to store-b (see the migration plan, Phase 1.1).
/wedding-invitation-frames could not be configured or bought on mobile
Found and fixed 2026-07-14. **Live on production.** On mobile the page rendered a frame preview, "Finished size: 16.0" × 12.3"", and then nothing — no price, no pickers, no Add to Cart. Customers could not configure or buy at all. The designer splits its mobile layout on `mobileView` from `useMobileViewToggle`: the controls half — every picker, the `PriceBox`, and Add to Cart — hides when the value is `"preview"`. That state initialises to `"preview"` and **the hook never changes it internally**; the only mutation path is the `setMobileView` it returns. The component destructured neither `setMobileView` nor `showMobileBar`, and rendered neither the toggle FAB nor the sticky bar. So `mobileView` was pinned for the life of the page. It was the only one of the 28 registered designers missing both affordances. **Nothing else caught this**: the route returns 200, the H1 prerenders fine (so SEO was unaffected), and it typechecks clean. It is only visible by actually driving the page at a phone viewport. FIX (PR #423): destructure the setter, render the sticky bar (which is what restores the purchase path — the page mounts the designer `embedded`, and the FAB is deliberately suppressed when embedded, matching every sibling) plus the FAB for non-embedded mounts. Verified in a real 390×844 Chromium viewport against both `next dev` and a production build: 0 Add-to-Cart affordances before, working sticky bar at $37.23 after. Guarded by `e2e/specialty-mobile-purchase-path.spec.ts`, proven non-vacuous (reverting the fix turns it red). ⚠️ Note for anyone re-running it: **HMR does not reliably propagate `packages/ui` edits into a running store-a dev server** — it will serve the old component and give a false pass. Restart the dev server after editing `packages/ui` before trusting any result.