FFrameCraft Ops

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/. — PROGRESS SINCE THE AUDIT (2026-07-24, PRs open on develop): (P0-SEO) the 301 long tail is CLOSED — PR #449 adds the Rule 4/5 + category/CMS rows (redirects.js +116, all 54 destinations curl-verified 200) plus a Rule 6 catch-all + case-normalization in middleware, so no legacy .html hard-404s (6 new tests, chains terminate at 200). GTM: the flagship injection is merged (8dace7e); PR #450 corrects the container so flagship no longer reports CPF traffic into CFS's container — it now needs its OWN CPF container id (provisioning, not code). Cross-brand accounts: the same-origin /bff proxy + an origin-aware OAuth redirect are built (PR #61 api + PR #451 monorepo, 109 + 1366 tests) — CPF/SBF now need only the Shopify redirect-URI registration + the enableAccounts flip. RB2B: the pixel plumbing landed dark on /business (PR #450), pending the team id + counsel wording. CPF checkout branding verified fully wired (code-identical to CFS). The top remaining launch blockers are unchanged: the store-a/store-b @sentry SSR crash, secret rotation, and the ToS/counsel signoff. — ALSO 2026-07-25 (more PRs open on develop): the flagship designer-API gap is CLOSED — PR #457 wires the 5 missing routes designers call (design-recommendations + upscale, both behind flags that are ON for flagship, plus proxy-image/frame-images/objects-upload-from-url); flagship now has 7 of 9 (business-quote + asset correctly absent). Designer image bugs fixed — PR #453 resolves raw image paths in ChallengeCoin/BulletinBoard/Heart (thumbnails + default previews were 404ing on the CDN); a scan of all ~30 specialty designers found no others. Mobile preview fixed — PR #454 + #455 replace the scroll-coupled shrink that collapsed the phone preview to a ~200px sliver with a fixed, viewport-capped height across all 29 specialty designers. The cpf-assets-not-uploaded blocker is largely cleared: the full out/cdn-assets staging set (flagship 410 + shared 55) was uploaded to R2 (add-only) and verified 200 — residual is a few never-staged assets (e.g. currency lifestyle) + confirming NEXT_PUBLIC_CDN_* on the Vercel deploys. Blockers still needing owner action: the @sentry store-a/b SSR crash, secret rotation, ToS/counsel signoff. — PROGRESS 2026-07-26 (pre-launch USER-EXPERIENCE SIMULATION, 2 rounds): ran two deep simulations against a LOCAL production build of the flagship + the PRODUCTION api (the deployable state — fixes land on develop unmerged, so a prod sim would re-find already-fixed issues), each rebuilt + Playwright/axe re-swept after fixing. ROUND 1 → PR #459 (MERGED to develop, merge-commit 775b187): 301 fixes incl. legacy MIXED-FRACTION size decode (11-3-4x16-1-2 → 11¾×16½, was dumping to /picture-frames), homepage <title> brand token, word-boundary meta truncation, removal of 'designer is being wired up' placeholder copy from 48 indexable pages, dedup of the ARIA-referenced duplicate <h1> id on ~75 pages, 4 hero images repaired from existing assets (7 more need client photography), first a11y batch (custom-mats criticals, specialty contrast ×31, main+canvas designer nested-interactive). ROUND 2 → PR #461 (OPEN on develop, 2 commits/56 files): a deeper sim caught what round 1 missed — (P1) HYDRATION crash on /framed-bulletin-boards (inline <style>{`…`} with double-quoted selectors → `"`→&quot; SSR-escape mismatch → React #425/#422; fixed via dangerouslySetInnerHTML + unquoted selectors); (P1) SOFT-404 (invalid/unbuilt /frames/sizes/* & /resources/* returned HTTP 200 not-found bodies → added dynamicParams=false, now real 404s); nested-interactive REMOVED from ALL remaining ~22 specialty designers + the nameplate header (closes the deferred-12 a11y debt); single <h1> across the 75-page frame-profile-hero (mobile copy → styled <p>) + 13 designer h1→h2 + blog markdown #→h2 + canvas depth Tabs→role=radiogroup (killed aria-valid-attr-value); 10 mat sliders + 4 icon buttons named; carousel tabIndex; aria-hidden decorative edges; star role=img; samples cards→label+checkbox; bulletin img→button; clampMetaDescription() public in @framecraft/seo funneling ~40 over-160 metas; mobile overflow (newspaper/blog/magazine). Verified: @framecraft/seo + packages/ui + flagship tsc clean, 102 flagship + 125 ui tests pass, jsx-a11y build warnings 10→0, hydration pageerrors 0 / mobile overflow 0 / multi-h1 75→0 on re-sweep. DOCUMENTED, deliberately not code-changed (need a design decision or are non-issues): link-in-text-block (~455 inline text-primary links across 68 files — wants a shared underlined prose-link style; a blanket change would wrongly underline nav/card links; low real impact as links are colour-coded); rotating search-placeholder contrast (passes ~6:1 at rest, only axe-flagged mid-fade = animation artifact); /scarf-and-sash-framing ~44px mobile overflow (minor). Adversarial verification also OVERTURNED 2 tempting fixes: /unfinished-picture-frames 404 is CORRECT (the real legacy URL already 301s), and ticket-frames' meta length was HTML-entity inflation, not truly long. Reviews.io PHASE 1 foundation built + gated OFF (env kill-switch + grounded REST client + clampMetaDescription; runbook at docs/reference/flagship-cpf/REVIEWSIO_INTEGRATION_RUNBOOK.md) on feat/reviewsio-integration — HELD pending review import + the public Store ID. Launch blockers still needing OWNER action are UNCHANGED: publish the Shopify store (cpf-headless still password-protected), restore the prod DB, set NEXT_PUBLIC_SITE_ORIGIN + verify canonicals/noindex, the @sentry store-a/b SSR crash, secret rotation, ToS/counsel signoff, 7 client hero photos. Detail: PR #459 (merged) + #461 (open); docs/reference/flagship-cpf/PRELAUNCH_SIMULATION_2026-07-26.md. — PROGRESS 2026-08-04 (client QA batch #195-#210 + an independent PRE-LAUNCH AUDIT, all landed DIRECT TO master): shipped Brian/Anthony's issue run — signature circle mat cut as a square hole so the accent read as a black box behind a round photo (both mat clip paths branched heart-vs-rect only); heart insert photos curated to the 16 whose composition survives the top-centre notch (every rejected one puts a head or a graduation cap exactly there); a $5 MINIMUM on the non-glare upgrade (Anthony's ruling) — the old T7a 'does not reproduce' verdict was a two-point sample at 16x20/32x40, and below ~16x20 the delta is cents that the round-up-to-.99 erases, so a 4x6 sonogram quoted $22.99 either way; needlework/diamond/cross-stitch/paint-by-number insert images stopped stretching (object-fit fill -> cover) with aspect-matched selection cutting worst-case crop 59%->25%; oversize mat filtering (32x40 vs 40x60) — flagship had FABRICATED 40x60 SKUs for the two texture mats by copying the 32x40 SKU, and 6 designers that can reach an oversize sheet never filtered (Currency/Stamp via CUSTOM sizes, TicketStub only on a narrow moulding — all three found by writing the guard, not by reading the code); hyphenated sizes ('16-1/16' parsed as 1, and the quiet one, '16-1/2' as 8 — a plausible wrong size straight to cart); the shadow-box colour cards, whose 9 CTAs all pointed at a bare /shadow-box-frames that mounted NO designer. — Then checked the independent 2026-08-03 pre-launch audit line by line against the code. Six findings CONFIRMED and fixed (commit 99c02e8): the sitemap listed zero product pages (73 now), which exposed that catalogue ids are LEGACY slugs so getFramePdpHref was emitting redirect sources — the audit's separate ~52-internal-301s finding, same root cause — plus six blog slugs already listed while redirecting away; titles losing their brand was ONE clamp cutting mid-brand with '|' missing from the separator strip, not six typos; garbled alt text was NOT the upstream name field the audit blamed but pre-rendered alts in our own frames.json generated against a superseded naming scheme (106 fixed on flagship, 106 on each of store-a/store-b, which the audit never checked); Product schema now inherits brandConfig's 4.8/4,280 (301 of 330 pages) and /jersey-frames gained the offers it never had; icons + apple-touch-icon on 541/541 pages; the auth-gated photo fetch no longer 401s for anonymous visitors. THREE audit claims were WRONG and deliberately not actioned: the mat multi-pack 'blocker' is Ben's own 2026-07-16 cutover decision (its 'truth build' is the abandoned v2 source; acting on it would roughly halve bulk mat revenue), the 36x48-vs-40x60 pricing anomaly does not reproduce across any of 84 SKUs, and the alt-text root cause was misdiagnosed. Net: the audit is reliable on symptoms and wrong on causes 3 for 3 — two of its prescriptions would have been wasted or harmful. REMAINING from it: the jersey/vinyl/graded-card base-price offsets (the one real blocker, needs Anthony's ruling), the /specialty-frames hero (absent from R2 — an upload, not code), alt-string LENGTH (1,642 of 2,694 over the 125-char window, worse than the audit's ~100), and 29 pages still hand-rolling Product schema.

CPF76% build
custompictureframes.com
launch ops 30%
4 blocker41 issues· 13 dec
CFS87% build
customframesizes.com
launch ops 15%
2 blocker22 issues· 2 dec
SBF43% build
shadowboxframes.com
launch ops 31%
3 blocker33 issues· 3 dec
Platform58%
cross-store infrastructure
2 blocker21 issues· 3 dec

Needs attention

top blocker & high-severity defects
CPF

Mock 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.

Blocker
CPF

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).

Blocker
CPF

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.

Blocker
CFS

/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).

Blocker
CFS

/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.

Blocker
SBF

Media/upload API routes missing -> upload/export/upscale/AI 404

Shared core/ui fetch same-origin /api/objects/upload(+from-url), /api/frame-images, /api/proxy-image, /api/upscale, /api/design-recommendations; store-b served none. Broke print-and-frame, nameplate upload, canvas CORS export, upscaling, AI recs. **FIXED 2026-07-14 (#415).** The 6 designer-critical handlers moved into a new shared `@framecraft/core/api-handlers` subpath; every app's route.ts is now a one-line re-export, so store-a, store-b and (later) flagship share ONE copy rather than three that drift — the same lesson the proxy-image SSRF (#413) taught, applied preventively. store-b now serves all 6; verified both apps build clean with the routes compiling as dynamic routes. Authored store-b's first `.env.local.example` (it had none) so a deployer knows to set the R2/Replicate vars; each unset var degrades to a clear 503, never a crash. The 7th route (/api/asset) is deferred on purpose — see SBF-3.

Blocker
All issues & decisions
Service healthChecking…0/11 operational · 0 degraded · 0 down