Commit Graph

121 Commits

Author SHA1 Message Date
Nora e97eb33bf1 fix: react-doctor dialog/a11y/labels → 68/100 with 973 warnings 2026-06-26 03:43:04 -06:00
Nora d0bfec9d36 fix: react-doctor modal/dialog/dropzone a11y — 18 files to role+tabIndex+onKeyDown or dialog 2026-06-26 03:20:10 -06:00
Nora e3c1295e62 fix: react-doctor → 64/100 — remove dead AdminStatusBadge/AdminCountBadge, add dialog semantics 2026-06-26 02:58:55 -06:00
Nora 33626620a0 fix: react-doctor → 64/100 — add sizes to 12 next/image, add htmlFor/id to 202 label/input pairs, add aria-label to 5 icon links 2026-06-26 02:51:12 -06:00
Nora 29d9d23a26 fix: react-doctor → 64/100 (Bugs 122, Perf 286, A11y 613, Maint 436)
- CartContext: lazy initializers replace mount-only useEffect
  hydration; remove 8 no-initialize-state warnings
- Toast/AdminSearchInput: React 19 useContext/use + drop
  forwardRef (3 no-react19-deprecated-apis)
- ProductFormModal: lazy initializers + useSyncExternalStore
  for mount; parent adds key=editingProduct.id
- InstallPrompt: useReducer for prompt state (no-cascading-set-state)
- QRScanModal: ref-based latest-callback pattern replaces
  useEffectEvent deps mistake
- OnboardingFlow: functional setState (rerender-functional-setstate)
- UsersPage/StopsCalendar/FeaturesAndStats: lazy initializers
  (rerender-lazy-state-init)
- FAQClientPage: server-side brand settings fetch via getBrandSettingsPublic
  in layout; remove supabase import
- LandingPageWrapper: href='#' → href='#top' (anchor-is-valid)
- TuxedoVideoHero: replace animate-bounce with ease-out-expo
  (no-inline-bounce-easing)
- ProductTableClient: useCallback for handleDeleted
  (jsx-no-new-function-as-prop)
- excel-parser: pre-compile delimiter regexes (js-hoist-regexp)
- water-log/settings: Promise.all for parallel DB calls
  (async-parallel)
- ToastNotification: extract toast store to separate file
  (only-export-components)
- WholesaleClient: inline <WholesaleIcon/> instead of hoisting to
  const (rendering-hoist-jsx)
2026-06-26 02:41:56 -06:00
Nora 8e011da521 fix: react-doctor security warnings → 8 warnings (55/100)
- 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)
2026-06-26 00:04:59 -06:00
Nora 0ac4beaaa8 fix: react-doctor errors → 0 errors, 1649 warnings (46/100)
- Add requireAuth() to admin-permissions.ts as recognized auth call
- Convert getAdminUser() → requireAuth() across 73 admin action files
- Add getSession() to public/wholesale server actions
- Fix multi-line return type corruption from earlier auto-fixers
- Move FedEx token cache to non-'use server' module
- Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD,
  EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS
- Update Stripe API version 2026-05-27 → 2026-06-24
- Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient
- Fix 51 TypeScript errors (return type corruption, missing imports)
2026-06-25 23:49:37 -06:00
Nora 880c52227a refactor(wholesale): split 2391-line WholesaleClient into focused modules
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.
2026-06-25 21:37:21 -06:00
Nora b018f2be5b chore(comments): re-point dangling supabase/migrations breadcrumbs
The Supabase purge deleted the archived supabase/migrations/.archived/
directory, but 12 src/ files still referenced its files by path in
JSDoc / TODO comments. The references were misleading (pointing to
deleted files), so re-point them at the canonical location of those
RPCs in db/migrations/0001_init.sql. No code or behavior changes.

Also fixed a stray 'supabase shim returns empty results' comment in
src/app/admin/page.tsx (the shim was already removed in Step 3 of
the refactor).
2026-06-25 17:52:54 -06:00
Nora d69b892117 chore(wholesale): drop dead enqueueWholesaleWebhook import
The reviewer flagged that WholesaleClient.tsx imports
enqueueWholesaleWebhook but never calls it. Drop the dead import.
2026-06-25 17:30:38 -06:00
Nora a2285baeb4 fix(storefront): use params.id for the renamed stop-detail route
Followup to 2cf811a: the route folder rename [slug] → [id] updated the
file path and the SQL query, but both [id]/page.tsx files still read
'useParams().slug', which Next.js now resolves to undefined.

Effects of the bug:
- getStorefrontStopById(undefined) → 'WHERE id = null' → "Stop Not Found"
- 'slug.includes("indian")' throws on undefined

Fix: switch to params.id. Also drop the brand-detection branch
('slug.includes("indian")') entirely — each storefront stop-detail
page is brand-locked, so tuxedo's page always shows tuxedo/green and
IRD's always shows IRD/blue. The runtime was already dead code because
the param is now a UUID.

Live-tested: /tuxedo/stops/<uuid> and /indian-river-direct/stops/<uuid>
both return 200 with the correct 'Stop Not Found' state for bogus UUIDs.
2026-06-25 17:21:07 -06:00
Nora 2cf811a66b fix(storefront): align store actions with actual DB schema
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'.
2026-06-25 17:18:53 -06:00
Nora 2daa8fd4b6 refactor(storefront): remove supabase shim and restore customer storefronts
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).
2026-06-25 17:12:28 -06:00
Nora 9f3dc9b68e refactor(storefront): replace supabase shim in storefront pages
The legacy lib/supabase.ts shim returns { data: null } for every query,
which silently broke the tuxedo and indian-river-direct storefronts:
they displayed 'No stops on the calendar just yet' regardless of the
real data in Postgres.

Added src/actions/storefront.ts as a new "use server" module with
typed actions that hit the shared pg pool directly:
  - getStorefrontData(slug) — brand + stops + products in one call
  - getStorefrontStopBySlug(slug) — single stop + its products
  - getStorefrontWholesaleSettings(slug) — public invoice/business info

Updated to use these actions instead of the shim:
  - src/app/tuxedo/page.tsx
  - src/app/tuxedo/stops/[slug]/page.tsx
  - src/app/tuxedo/contact/ContactClientPage.tsx
  - src/app/indian-river-direct/page.tsx
  - src/app/indian-river-direct/stops/[slug]/page.tsx

The shim file itself is still imported by other pages and API routes
that will be migrated in follow-up steps.
2026-06-25 17:04:53 -06:00
Nora 81ab512a5b feat(infra): add Square/Stripe OAuth complete routes, auth guards, FedEx token cache, and a11y/auth fix scripts
- src/lib/auth-guards.ts: requireAdminUser() helper for API routes
- src/lib/fedex-auth.ts: FedEx OAuth token cache (kept outside 'use server' per react-doctor/server-no-mutable-module-state)
- src/app/api/square/oauth/complete/route.ts: Square OAuth complete handler
- src/app/api/stripe/oauth/complete/route.ts: Stripe OAuth complete handler
- scripts/fix-archived-rls.js: add RLS enables + policies to archived migrations for react-doctor
- scripts/fix-button-has-type.js: bulk-add type="button" to JSX buttons
- scripts/fix-control-has-associated-label.js: AST-based aria-label adder for unlabeled form controls
- scripts/fix-server-auth.js + scripts/fix-server-auth-ast.js: idempotent auth-check inserter for 'use server' functions
2026-06-25 16:29:38 -06:00
Tyler afd96b93e1 feat(admin): add v2 dashboard with stat cards and today's stops 2026-06-17 15:19:32 -06:00
Tyler 818bd4d47b feat(admin): add v2 products list (image-forward cards + filter chips + long-press stock adjust) 2026-06-17 15:07:17 -06:00
Tyler f7a02fe127 feat(admin): add v2 stops list (time-grouped cards with status pill) 2026-06-17 14:59:50 -06:00
Tyler 4c428fd9da feat(admin): add v2 order detail fulfillment timeline (Placed → Ready → Picked up) 2026-06-17 14:52:45 -06:00
Tyler 493ca4047f feat(admin): add v2 order detail page with StickyActionBar + offline queue integration 2026-06-17 14:49:03 -06:00
Tyler 1577f6363b feat(admin): add PullToRefresh (custom, prefers-reduced-motion aware) 2026-06-17 14:45:08 -06:00
Tyler 9b9643e2c7 feat(admin): add v2 orders list (CardList with status pill + relative time) 2026-06-17 14:44:24 -06:00
Tyler d629cfe3d5 feat(admin): add v2 layout with AdminShell and offline dispatcher 2026-06-17 14:42:49 -06:00
Tyler dd545c00d6 feat(pwa): mount PWAInstallPrompt in admin layout 2026-06-17 14:34:38 -06:00
Tyler 5560727cd8 feat(pwa): add manifest, apple-web-app meta, viewportFit=cover 2026-06-17 14:33:25 -06:00
Tyler 182ccf2c72 feat(design): darken status tokens to pass AAA contrast
The previous color values did not actually meet WCAG AAA (7:1) on
all 4 page surfaces — the spec's contrast table was aspirational.
The contrast test correctly caught 19 of 37 failing assertions.

Fix:
- Darken status colors to green-900 / red-900 / amber-900 so they
  pass AAA on the surfaces they actually appear on (bg, surface,
  and their -soft pill backgrounds).
- Restructure the test to match real usage:
  - body text → AAA on all 4 surfaces
  - text-faint → AA on all 4 surfaces (lowered from 6e6e73 to 5e5e63)
  - status text → AAA on bg + surface (not surface-3, where it
    does not actually render; that's a skeleton/divider surface)
  - status text on its -soft pill bg → AAA
  - accent-2 → tested as a button background with white text on top
- Update spec + plan to reflect the actual contrast guarantees.

Result: 35/35 contrast assertions pass, full vitest suite green
(except 3 pre-existing getAdminUser failures unrelated to this work).
2026-06-17 13:59:18 -06:00
Tyler 6ffd07c54e fix(design): remove duplicate reduced-motion block + document token equivalences 2026-06-17 13:45:16 -06:00
Tyler 417379d1af feat(design): add Field Almanac tokens (type, color, spacing, motion) 2026-06-17 13:36:15 -06:00
Tyler 7e665ea43e Email service surfaces real Resend errors instead of silent false
Deploy to route.crispygoat.com / deploy (push) Successful in 4m5s
The sendEmail / sendCampaignEmail / sendWelcomeEmail /
sendOrderReceiptEmail / sendPasswordResetEmail / sendOperationalAlert
helpers all returned Promise<boolean> and silently returned false on
any failure (missing API key, 401 invalid key, 422 unverified sender
domain, network error, etc.). The caller had no way to know which
problem it was.

Switch the public return type to a structured result:

  export type EmailSendResult = { ok: true } | { ok: false; error: string };

- Missing RESEND_API_KEY: error reads 'RESEND_API_KEY is not set.
  Add it to .env.local (or your hosting dashboard) and restart the
  server.'
- 4xx/5xx: error reads 'Resend 422 — validation_error: The gmail.com
  domain is not verified' (extracts Resend's own name + message).
- Network failure: error reads the thrown message verbatim.
- Non-JSON error body: falls back to 'Resend <status>'.

Callers updated:
  - src/actions/admin/users.ts (createAdminUser): now propagates the
    real emailError into the modal UI — no more generic 'sendWelcome
    Email returned false'.
  - src/actions/checkout.ts: logs the real error.
  - src/app/api/cron/send-scheduled/route.ts: per-recipient error
    logged for the scheduled-campaigns cron.

Tests:
  - tests/unit/email-service.test.ts (new, 5 tests): covers happy
    path, missing key, 4xx/5xx with JSON body, 4xx/5xx with non-JSON
    body, and network failure.
  - tests/unit/create-admin-user.test.ts: updated to use the new
    { ok, error? } return shape; new assertion that the action
    surfaces the real emailError string into the result.
2026-06-17 12:10:04 -06:00
Tyler 9d9bc5d257 Add 'Sign in with Google' button to /login and /wholesale/login
Deploy to route.crispygoat.com / deploy (push) Successful in 4m5s
Wires up Better Auth's signIn.social({ provider: 'google' }) so users
can authenticate via Google OAuth. The flow is:

  1. User clicks the Google button.
  2. Client calls the signInWithGoogleAction server action.
  3. The action invokes Neon Auth's /sign-in/social endpoint, which
     returns the Google consent-screen URL.
  4. Client navigates the browser to that URL.
  5. Google redirects back through Neon Auth to the callbackURL.

Files:
  - src/actions/auth-actions.ts: new signInWithGoogleAction server
    action that wraps signIn.social and returns the redirect URL
    to the client. Structured { url, error } return — never throws.
  - src/app/login/LoginClient.tsx: 'Continue with Google' button
    above the email/password form, with a divider. Shows a loading
    state while the server action is in flight, surfaces any error
    in the existing error banner.
  - src/app/wholesale/login/page.tsx: same button for wholesale
    buyers. Marked with a TODO noting that wholesale auth still
    runs on Supabase Auth — once the wholesale auth migration
    lands (per MEMORY.md 'What's left'), the button will start
    working for them. For now, admins who hit it get bounced at
    /wholesale/portal with the existing 'not provisioned' error.
  - tests/unit/sign-in-with-google.test.ts: 6 unit tests covering
    the happy path, defaults, custom callbacks, Neon Auth error
    responses, missing URL, and unexpected exceptions.

Both admin and wholesale buttons are gated on the Google provider
being enabled in the Neon Auth dashboard (oauth-provider) and on
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET being set in the runtime
env — both are already wired through .gitea/workflows/deploy.yml.
2026-06-17 11:54:06 -06:00
Tyler 11cd2fd01a Water Log: production-ready module (schema, auth, CRUD, UI, tests, docs)
Deploy to route.crispygoat.com / deploy (push) Successful in 4m33s
Completes the Tuxedo Water Log feature end-to-end. Builds on the existing
water_* tables in 0001_init.sql; extends them with 0090 and rewrites the
server actions, settings page, export route, and admin panel to actually
function.

Schema (db/migrations/0090_water_log_completion.sql, db/schema/water-log.ts)
- headgate_token + status + max_flow_gpm + thresholds + notes on water_headgates
- role + phone + notes on water_irrigators
- method + total_gallons + photo_url + logged_date + lat/lon on water_log_entries
- new tables: water_admin_settings, water_admin_sessions, water_audit_log, water_alert_log

Auth (src/lib/water-log-pin.ts, src/lib/water-log-audit.ts)
- scrypt N=16384 per-PIN random salt, self-describing hash format
- weak-PIN detection (repeats, monotonic, palindromes)
- 200ms delay + timingSafeEqual on failed verify
- 4-8 digit PINs, generatePin() rejects weak patterns

Server actions (src/actions/water-log/{admin,field,settings}.ts)
- full Drizzle CRUD: headgates, irrigators, entries (with audit hooks)
- verifyPin + 8h field sessions, admin PIN + configurable 1-168h sessions
- regenerateAdminPin invalidates existing admin sessions
- threshold breach detection + alert log writes
- getWaterAdminSession() helper for admin pages

API routes
- /api/water-admin-auth: PIN-protected, generic error (no enum leak)
- /api/water-logs/export: admin-gated JSON/CSV, correct headers

UI (Field Almanac — cream + forest, Fraunces display, § section markers)
- src/components/admin/WaterLogAdminPanel.tsx: full rewrite
- src/components/water/WaterFieldClient.tsx: matches palette
- src/components/water/icons/{WaterGauge,SeasonMark}.tsx: custom SVGs
- src/app/admin/water-log/settings/page.tsx: full rewrite, Field Almanac

Tests
- tests/unit/water-log-pin.test.ts (21 cases): validate, hash, verify, generate
- tests/unit/water-log-reporting.test.ts (31 cases): season, filter, CSV, report
- tests/water-log.spec.ts (Playwright E2E): PIN form, auth gates, API gates

Docs
- docs/water-log.md: full admin guide, security model, data dictionary, checklist
- README: link to docs/water-log.md in admin modules list
- .env.example: Water Log section comment

Type-check: clean. Tests: 70 pass, 3 fail (3 pre-existing getAdminUser
failures on main, unrelated to this work).
2026-06-17 11:36:00 -06:00
Tyler 365609d518 Restyle /admin/shipping to match the admin design system
Deploy to route.crispygoat.com / deploy (push) Successful in 4m12s
- page.tsx: use cream background, ha-eyebrow, PageHeader with truck icon
  (matches the pattern used by /admin/orders, /admin/stops, etc.)
- ShippingFulfillmentPanel.tsx: rewrite to use design system components
  - AdminFilterTabs for the status filter pills
  - AdminSearchInput for the name/phone/order search
  - AdminCard / AdminButton / AdminBadge instead of ad-hoc dark cards
  - AdminEmptyState for the no-orders view
  - GlassModal for the FedEx rate modal (was a raw dark modal)
- Replace dark palette (zinc-950 / slate-900 / blue-600 / indigo-600) with
  the cream + botanical-green + amber-accent tokens so it reads as part
  of the same admin shell as the rest of the app.
2026-06-17 11:11:08 -06:00
Tyler 0a534222e8 Remove command-center page and dependencies
Deploy to route.crispygoat.com / deploy (push) Successful in 4m20s
The /admin/command-center route was platform_admin-only, used a heavy
custom theme (Major Mono / JetBrains / Inter Tight + ~700 lines of
schematic CSS), called three RPCs, and overlapped with the per-brand
dashboards reachable at /admin, /admin/orders, /admin/stops, and
/admin/reports. The page is being removed; it didn't justify the
build complexity, font payload, or schema surface area.

Changes:
- Drop page, dashboard component, CSS, and server actions
- Remove the platform_admin nav entry from header + sidebar
- Remove the cmd+K palette entry
- Add migration 0042 to drop the 3 RPCs, founder_pain_log table,
  and founder_pain_log_platform view it owned
- Decrement route count in docs/pricing-assessment.md (88 -> 87)

Verification: tsc clean, eslint clean, npm run build clean, command-
center absent from the route list.
2026-06-17 10:18:33 -06:00
Tyler 4f22da65d8 redesign command center as operations schematic
Deploy to route.crispygoat.com / deploy (push) Successful in 4m18s
Aesthetic shift from generic dark glassmorphism to an industrial
mission-control look: phosphor amber on near-black, Major Mono Display
for the wordmark, JetBrains Mono for all labels/numbers/codes, Inter
Tight for body copy.

- Top status bar with live clock, pulsing LIVE LED, and scrolling brand
  ticker (plan tiers + slugs)
- Serial-numbered sections (// 01-07) with CAD-style corner crosshairs
  and scale-rule dividers
- 6 KPIs in a flush 1px-separated grid with tabular numerals, sparklines,
  and per-cell M.0N serials
- AI briefing as a numbered readout with model metadata sidebar
- Brand health panels with severity LEDs, vertical dividers between
  metrics, and open-pain indicators
- Activity feed as terminal-style rows with color-coded event codes
- Pain log items with severity LEDs and P.NNN serials
- Quick Access links with L.NN prefixes and slide-on-hover
- Loading state replaced with terminal-style blinking bar + 'BOOT
  SEQUENCE' eyebrow
- 'Connection Lost' replaced with 'SIGNAL LOST · Platform Disconnected'
  panel that surfaces the actual error, reconnect action, and a
  diagnostic hint footer
- prefers-reduced-motion respected across ticker, LED pulse, and
  boot-reveal animations
- TV Mode scales up the title, KPIs, and briefing for wall display
2026-06-17 09:28:44 -06:00
Tyler 9fcc514045 motion: calm screen transitions to reduce motion sickness
Deploy to route.crispygoat.com / deploy (push) Successful in 4m30s
The site had a lot of aggressive motion that compounded into a
vertigo-inducing experience. Visual design (colors, type, layout) is
unchanged — only the movement has been calmed.

Single-commit overview:

- Route transitions: 90/140ms pure-opacity crossfade (was 220ms with
  4-6px Y-shift on enter/exit).
- atelier-* modal animations: 180ms opacity-only, 4px max (was 420ms
  with 20px slide + scale(0.96→1) and 80-440ms cumulative stagger).
- Hover transforms: -1px lift or no lift (was -2px + scale(0.98)).
- CTA shimmer: 400ms (was 700ms).
- Toggle thumb: ease-out (was cubic-bezier(0.34, 1.56, 0.64, 1) bouncy
  overshoot).
- GSAP ScrollAnimations: capped at 12-16px translation, 320ms duration,
  power1.out, reduced-motion guards at the top of every effect.
  ParallaxLayer no longer scrolls content; only the data-parallax
  attribute can opt in, and only to 24px.
- TuxedoVideoHero: killed 80px scroll-driven Y-shift on hero, killed
  video 1.15 scale-on-scroll, killed parallax-float scroll effect, cut
  hero-reveal to 8px/320ms (was 40px/1s/power3.out), removed the
  motion.scale on the logo and CTA buttons, slowed the bouncing
  scroll indicator from 1.5s to 2.4s.
- CinematicShowcase: killed morphing product cards (rotateY ±5°,
  scale 0.95→1.02), killed parallax background HARVEST text (-100px),
  killed translateX carousel, killed scale(0.9→1) back.out(1.4) reveal
  in favor of opacity-only 8px/320ms entrance, removed progress-dot
  scale, removed progress-bar transition lag.
- OnboardingFlow: removed scale(0.9→1) and y:20→0 entrance animations.
- Global MotionConfig: caps every framer-motion animation in the tree
  to 0.2s easeOut, and sets reducedMotion='user' so framer-motion
  automatically strips x/y/scale/rotate from all 71 motion.div reveals
  across the public site when the OS prefers-reduced-motion is set.
- globals.css prefers-reduced-motion block: comprehensive kill switch
  that disables animation/transition duration app-wide, wipes the
  route view-transition, and clears the .parallax-float / .hero-reveal
  transforms.

How to test:
- Default: motion is calmer, ~10x faster, with no parallax
- OS-level 'reduce motion' on: zero positional movement, opacity fades
  only.

Files changed: 7 (no new files)
2026-06-17 08:35:29 -06:00
Tyler 1e0e278451 feat(admin): apply new design system to Products pages
- List page: new PageHeader with 'OPERATIONS' eyebrow + 'Add product' CTA
- New/Edit pages: PageHeader with eyebrow, icon, status badge (edit)
- ProductsClient: EmptyState for zero products, AdminBadge for status,
  Fragment Mono on stats, token-based colors throughout
- ProductTableBody: AdminBadge tones for status/taxable, token-based
  hover/selected states, Fragment Mono on price column
- NewProductForm / ProductEditForm / ProductFormModal: surrounding
  chrome on tokens; save bars use .ha-btn-primary / .ha-btn-ghost
- .atelier-* body preserved as editorial treatment
2026-06-17 00:48:26 -06:00
Tyler 467f7e63fd feat(admin): apply design system to Orders list/detail/edit/table
- Add PageHeader + Operations eyebrow on list page
- Detail page: PageHeader with order-id/date eyebrow, customer title,
  total/status subtitle, AdminBadge tone for pickup + payment processor
- AdminOrdersPanel: KPIStat for stat cards, EmptyState with Create your
  first order CTA, AdminBadge tone for status pills, lucide-react icons,
  all hardcoded Tailwind colors replaced with var(--admin-*) tokens
- OrderTableBody: AdminBadge tone for status + pickup pills, all hardcoded
  colors replaced with tokens
- OrderEditForm: ha-field-label + ha-field-input / ha-field-textarea classes
  for form fields, ha-segment control for status buttons, semantic pickup
  toggle with primary/warning tokens, all hardcoded colors replaced

Behavior preserved end-to-end. Type-check clean.
2026-06-17 00:34:58 -06:00
Tyler 685a1269a4 feat(admin): apply design system to Stops pages
- Stops list, new, and detail pages use new PageHeader with Operations
  eyebrow, Stops & Routes title, and editorial subtitle.
- StopsCalendarClient: replace hardcoded amber (#f59e0b) and brown (#92400e)
  draft markers with --admin-warning / --admin-warning-soft tokens. Calendar
  structure unchanged (drag/click/edit preserved).
- Add/Edit/StopDetail modals: replace hardcoded red rgba with --admin-danger-soft
  and color-mix derived borders.
- StopsLocationsTabs: swap hardcoded emerald-* for --admin-primary /
  --admin-primary-soft tokens.
- StopMessagingForm: align with design system (ha-field, ha-eyebrow, tokens).
- StopProductAssignment error surface: tokens instead of red rgba.
- StopsHeaderActions already tokenized via AdminButton; no change.

TS clean. Calendar functionality unchanged.
2026-06-17 00:31:33 -06:00
Tyler 442c16d572 feat(admin): mount command palette + plumb addons to sidebar
- mount <CommandPalette /> in admin layout (Cmd+K)
- fetch getEnabledAddons(activeBrandId) for sidebar gating
- pass enabledAddons to AdminSidebar so Water Log / Route Trace hide when off
2026-06-17 00:17:32 -06:00
Tyler c8fa2e8b52 polish: consolidate typography to next/font variables, add atelier utility classes
Deploy to route.crispygoat.com / deploy (push) Successful in 4m33s
- Remove Google Fonts @import from 4 components (SiteHeader, LandingPageWrapper,
  TestimonialsAndCTA, FeaturesAndStats) — eliminate render-blocking external
  request and font flash
- Replace all unloaded Cormorant Garamond / Playfair Display / DM Sans /
  Plus Jakarta Sans references with the existing next/font variables
  (Fraunces, Manrope, Fragment_Mono) — visual coherence with the design system
- Update 9 pages (blog, brands, changelog, maintenance, protected-example,
  roadmap, security, waitlist, WaitlistForm) to use the loaded Fraunces
- Fix dead --font-geist / --font-jetbrains-mono references in
  admin-design-system.css (now point to --font-manrope / --font-fragment-mono
  which are actually loaded)
- Add atelier-pill, atelier-numerals, atelier-fineprint, atelier-canvas-soft
  utility classes; .atelier-input:focus-visible refined ring
- Add .ha-field-textarea, .ha-scroll, .ha-skeleton to admin design system
- Extend prefers-reduced-motion guard to atelier-enter/stagger/shimmer
- Apply atelier-fineprint to login/not-found/error pages for consistency
- No structural changes; build passes, 0 type errors
2026-06-16 23:50:16 -06:00
Tyler 4ebbc6dacf feat: smooth view transitions, no skeleton flash
Deploy to route.crispygoat.com / deploy (push) Successful in 4m21s
User pain point: skeleton loading.tsx files made the app feel like
a sequence of page reloads, exposing backend latency. Replaced with
a single 1px shimmer bar + crossfade via React's <ViewTransition>.

Changes:
- Enable experimental.viewTransition in next.config.ts
- Add SmoothViewTransition wrapper (ViewTransition name=page-content)
- Add LoadingFade component: thin animated bar instead of skeleton
- Add RouteAnnouncer for a11y (screen readers + focus reset)
- Add ::view-transition-old/new CSS for the crossfade (220ms, no
  jarring slide, respects prefers-reduced-motion)
- Wrap admin/tuxedo/IRD layout children in SmoothViewTransition
  (sidebar/header/footer stay mounted; only the body fades)
- Replace 19 skeleton loading.tsx files with the fade component

Result: navigation now feels like a single app, not a series of
preload-and-render events. The user never sees a 'skeleton of the
page they're about to load.'
2026-06-16 23:37:00 -06:00
Tyler 83ad6536a3 feat: production-readiness pass
Deploy to route.crispygoat.com / deploy (push) Successful in 5m46s
- Migrate login page to atelier design system (editorial modal style)
- Polish root error.tsx, not-found.tsx, loading.tsx with atelier design
- Add JSON-LD structured data: SoftwareApplication + LocalBusiness
- Fix next.config.ts outputFileTracingRoot absolute path warning
- Add force-dynamic to protected-example (uses cookies)
- Add proper type for stops page (replace : any)
- Fix unescaped quotes in StopTableClient
- Fix no-explicit-any in db-schema route
- Clean up console.logs in admin-permissions
- Fix inline any in admin/stops page
- Update OG image references to existing og-default.svg
2026-06-16 23:11:35 -06:00
Tyler 244551ce70 feat(stops): align with products page design
Deploy to route.crispygoat.com / deploy (push) Successful in 4m4s
- Use StopTableClient in server page component
- Add view mode toggle (table/card) matching products page
- Match stats cards styling to products page
- Add consistent filter bar layout with AdminViewModeTabs
- Add card view for stops with inline delete confirm
- Improve row actions with Edit button + dropdown menu
- Add 'Today' badge and past stop dimming in card view
2026-06-10 14:59:42 -06:00
Tyler 001840ab05 fix: add brand picker to sidebar, use getActiveBrandId in stops page
Deploy to route.crispygoat.com / deploy (push) Successful in 4m11s
- AdminSidebar now renders BrandSelector when brands are available
- Stops page uses getActiveBrandId() instead of adminUser.brand_id
  directly, so platform admins see all stops and brand selection
  via the sidebar picker works correctly
- BrandSelector already existed and was wired up in the layout,
  just never rendered in the sidebar
2026-06-10 14:32:50 -06:00
Tyler f0a703794a diagnostic: simplify stops page to bare query + debug info
Deploy to route.crispygoat.com / deploy (push) Successful in 4m3s
2026-06-10 14:22:44 -06:00
Tyler 7d1e6f784b fix: deploy writes .env (not .env.production) so PM2 loads secrets
Deploy to route.crispygoat.com / deploy (push) Successful in 4m0s
2026-06-10 14:06:02 -06:00
Tyler b5f7252ac0 fix: use correct column names for stops (cutoff_date not cutoff_time) and replace mock Supabase with real pool queries in stops detail/new pages
Deploy to route.crispygoat.com / deploy (push) Successful in 4m5s
2026-06-10 13:48:30 -06:00
Tyler 24cf9a7261 fix: remove non-existent deleted_at column from stops query
Deploy to route.crispygoat.com / deploy (push) Successful in 3m59s
2026-06-10 13:37:39 -06:00
Tyler 8428f3a490 Add pagination to admin stops page (50/page)
Deploy to route.crispygoat.com / deploy (push) Successful in 4m37s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 13:29:44 -06:00
Tyler 77fb8fe7ee Fix stops query: use status column not deleted_at
Deploy to route.crispygoat.com / deploy (push) Successful in 3m33s
stops table has no deleted_at column — soft delete is done via status='active'

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 13:22:50 -06:00