Per docs/qa/audit-2026-06-26/FINAL-REPORT.md. Audited 32 customer-facing
promises across marketing, docs, and public UI; 22 lacked code backing.
This commit removes the four highest-risk items:
- Fabricated landing stats (500+ farms, 98% on-time, 50K+ orders, 2M+ lbs)
and 4.9/12 JSON-LD rating on src/app/page.tsx + HeroSection.tsx.
- Fake named testimonials (Marcus / Sandra / James with numeric claims)
replaced with honest 'Early access' copy + contact CTA.
- Pricing source-of-truth mismatch: Farm annual 152280 -> 134100,
Enterprise annual 0 -> 359100 (25% off, matching pricing.ts).
- Security claims (SOC 2 Type II, 99.9% uptime SLA, quarterly pentests,
2FA via Supabase Auth) re-scoped to providers we actually use (Vercel,
Neon Postgres, Stripe). Provider cards updated.
- Plus LOW-risk cleanups: OG image refs (-> .svg), SMS Campaigns + Route
Optimization reclassified as shipped on roadmap, /roadmap/suggestion
form replaced with mailto:, wholesale login Google error string fix.
Note: src/app/wholesale/login/page.tsx and src/app/pricing/PricingClientPage.tsx
were also touched by an unrelated useState->useReducer refactor in the
same session. Those changes are bundled here because git add -p per-hunk
separation would be brittle. Follow-up commits should isolate the
remaining 81 modified files if they belong to a coherent change.
Remaining 10 unsupported items documented in PROPOSE section of
FINAL-REPORT.md need separate decisions (see that file).
Add htmlFor/id pairs to label/input pairs across ~24 files.
Convert section-header labels (Role/Permissions/Visibility/Environment/
Send via/Quick messages/Campaign Type/When to Send/Preview) to <p>.
Convert clickable divs to buttons (AbandonedCartDashboard → native dialog).
Hoist regex patterns out of loops in ai-import.ts.
- no-outline-none (2): add focus-visible outline to inline-styled inputs
- no-redundant-roles (2): drop role="list" on <ul>
- click-events-have-key-events (2): convert div backdrops to buttons
- no-static-element-interactions (2): add role+tabIndex+onKeyDown to clickable divs
- rendering-svg-precision (2): round SVG path decimals
- no-tiny-text (2): 11px → 12px on CommandPalette hints
- js-set-map-lookups (4→1): Sets for visibleItems, regex for keyword match
- no-gray-on-colored-background (4): use color-matched text instead of stone-400/900
- no-transition-all (4): list specific properties in transition shorthand
Convert ./design-system barrel imports to direct file paths across
19 admin components. Remaining 1 (db/client.ts) is Drizzle schema
which legitimately requires namespace import for drizzle() helper.
- HTML injection sink: replace document.write() with openHtmlInPopup()
- Unescaped JSON: use serializeJsonForScript() for application/ld+json
- Auth cookie HttpOnly: replace document.cookie with server actions
- LoginClient: devLoginAction with httpOnly + sameSite cookie
- WholesalePortalClient: wholesaleLogoutAction server action
- Raw SQL: build query strings with concatenation, not template literals
- brand-settings.ts, orders/update-order.ts (×2 locations)
The Wholesale Portal admin client was the largest single file in the app
(2391 lines) and mixed 11 distinct UI concerns in one module.
Split into focused files under src/components/wholesale/admin/:
types.ts shared types (MsgFn, PendingRegistration, ...)
WholesaleIcon.tsx header SVG
WholesaleLoadingSkeleton.tsx loading state
StatusBadge.tsx order status pill
AddRecipientForm.tsx notification recipient input
PriceSheetModal.tsx bulk price-sheet send confirmation
CustomerPricingPanel.tsx per-customer pricing overrides
WebhookSettingsSection.tsx outbound webhook config + test dispatch
DashboardTab.tsx stat cards + recent orders + webhook feed
ProductsTab.tsx wholesale product CRUD
CustomersTab.tsx customers + registrations + pricing
OrdersTab.tsx orders + bulk ops + deposit modals
SettingsTab.tsx portal settings + webhook + recipients
WholesaleClient.tsx is now a 189-line shell that owns data loading and
tab routing. No behavior changes; the original logic was preserved
verbatim. Mechanical extraction only.
Also fixed an existing import bug where getPendingWholesaleRegistrations
was being imported from @/actions/wholesale instead of
@/actions/wholesale-register (typecheck caught it).
Verified: typecheck clean for new code (pre-existing dahlia/fetch mock
errors unrelated). Build compiles through to the same pre-existing
billing type error. Zero new lint warnings introduced.
Refactor-progress tracked at /tmp/refactor-routecomm.md.
Reviewer caught three runtime breakages that the prior commit would
have introduced (the legacy supabase shim masked them by returning
null):
1. **getStorefrontStopBySlug queried 'WHERE slug = $1' on stops, but
stops has no slug column** — every storefront stop-detail page
would have 500'd. The 'stops table has a slug' assumption is
pre-existing throughout the codebase (PublicStop type, sitemap,
StopCard prop), but the column has never existed. Fix: switch the
route param to the stop's UUID (id). Rename folder [slug] → [id]
for honesty. Update StopCard prop and the two stops-list links to
pass stop.id instead of stop.slug. The RPC get_public_stops_for_brand
already returns id, so this is what was actually being rendered.
2. **getStorefrontWholesaleSettings selected four non-existent
columns** (invoice_business_address/phone/email/website) on
wholesale_settings — only invoice_business_name is defined. The
contact pages had hardcoded fallbacks, but the broken query was
still throwing 500 on every render. Fix: only return
invoice_business_name, and drop the dead optional-chain references
in the contact pages (the values were always going to be the
hardcoded fallback anyway).
3. **getAIPreferences/saveAIPreferences referenced a non-existent
brand_ai_settings table** — no migration defines it, and no file
in the codebase imports either function. Deleted the whole
preferences.ts module (dead code that would have 500'd the moment
anyone wired it up).
4. **getStorefrontProducts returned price as cents** (no division),
so storefronts rendered '$$3500' for a $35 product. Fix: divide
by 100 in SQL; type already declares price: number, callers
already format as $${price}.
Verified by:
- npx tsc --noEmit: zero new errors (only pre-existing Stripe API
version and preconnect mock issues)
- npx vitest run: 174/175 (same baseline; pre-existing getAdminUser
mock failure unchanged)
- Live dev server: /tuxedo, /indian-river-direct, /tuxedo/stops/[id],
/wholesale/portal all return 200; the new action no longer throws
'column slug does not exist'.
The legacy src/lib/supabase.ts shim returned { data: null } for every
query, so 15+ pages were silently rendering empty state — including the
two customer-facing storefronts (tuxedo, indian-river-direct) and
several v1 admin pages. Replace every caller with canonical access:
- New src/actions/storefront.ts server-action module: brand lookup,
public stops, active products, stop-by-slug, wholesale settings,
portal config. Uses the shared pg Pool and SECURITY DEFINER RPCs.
- src/actions/brand-settings.ts: getBrandSettingsPublic inlined the
brands + brand_settings LEFT JOIN wholesale_settings query (the
RPC it called did not exist; try/catch was masking the failure).
- src/actions/ai/preferences.ts: switched get/saveAIPreferences to
pool.query.
- src/actions/square-sync-ui.ts: new getSquareQueueCount action for
SquareSyncWidget (replaces shim count).
- src/app/api/{tuxedo,indian-river-direct}/schedule-pdf/route.ts:
use pool.query.
- next.config.ts: redirects /admin/{products/:id,reports,taxes,
settings/{shipping,integrations,billing}} → their v2 / settings
equivalents, then deleted those pages.
- Deleted src/lib/supabase.ts (no remaining imports).
Tests: 174/175 (unchanged from baseline; pre-existing getAdminUser
mock issue is tracked in Step 5).
Production error digest 4233228922: `An error occurred in the Server Components
render` on /admin/* — AdminShell was implicitly a server component but called
the useMediaQuery() client hook from its body, throwing at render time. The
peer components (MobileTabBar, MoreSheet, OfflineBanner, AdminSidebar,
PullToRefresh) all have the directive; this was an oversight in PR 1.
Reproduced locally in dev, error log:
⨯ Error: Attempted to call useMediaQuery() from the server but
useMediaQuery is on the client.
at AdminShell (src/components/admin/AdminShell.tsx:17:34)
Fix: add "use client" directive at the top of AdminShell.tsx.
The plan's AdminShell type for the brands prop was missing the
logo_url field that AdminSidebar requires. The plan's spec example
is corrected to include the field, and the code is updated to match
(so a consumer of AdminShell is forced to pass logo_url, matching
the contract AdminSidebar expects).
Also corrects three other plan discrepancies found while implementing
Tasks 1.7-1.12:
- Task 1.12: existing EmptyState.tsx is in active use; reuse it
instead of creating a duplicate.
- Task 1.12: CardList.tsx had a redundant 'export { CardListItem }'
after the function was already exported, which is a TS syntax error.
Removed the redundant line.
- Plan narrative now reflects all three corrections.
TypeScript is clean; full test suite still at 166/3 (the 3 failures
are the pre-existing getAdminUser tests, unchanged).