Commit Graph

98 Commits

Author SHA1 Message Date
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
Tyler eabc709076 Fix admin stops page: replace mock supabase with direct pool query
Deploy to route.crispygoat.com / deploy (push) Successful in 4m2s
The page was importing from @/lib/supabase which is a mock client that
returns empty results. Replaced with pool.query() against the real
Postgres stops table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 13:17:39 -06:00
Tyler 0cf2ee7948 Admin dashboard: fix stats waterfall, redesign layout, add animations
Deploy to route.crispygoat.com / deploy (push) Failing after 3m13s
- Fetch dashboard stats server-side in admin/page.tsx to eliminate
  client-side useEffect waterfall and the '—' placeholder flash
- Pass pre-fetched stats as props to DashboardClient
- Lazy-load UpgradePlanModal via next/dynamic (only loads when needed)
- Redesign stats cards with compact layout, smaller icons, Fraunces serif values
- Improve Quick Actions + Usage row: side-by-side on desktop, stacked mobile
- Clean up Recent Orders as a proper list with icon/name/time/amount/badge
- Add staggered fade-up entrance animations (0/60/120/180/240ms delays)
- Consolidate loading.tsx skeleton to match actual dashboard structure
- Mobile-responsive: 2-col stats on mobile, stacked usage, collapsible header
2026-06-10 12:45:08 -06:00
Tyler 4909a78aca stops import: swap OpenAI for MiniMax as default AI provider
Deploy to route.crispygoat.com / deploy (push) Successful in 5m27s
2026-06-10 10:58:37 -06:00
openclaw 91ba7b5c5c fix(deploy): add health/db-schema guard + curl gate; ship migration assets; document prod bootstrap (executed per systematic-debugging plan)
Deploy to route.crispygoat.com / deploy (push) Has been cancelled
2026-06-09 15:45:58 -06:00
openclaw d312783f3a fix: admin auth for prod Neon Auth deployment
Deploy to route.crispygoat.com / deploy (push) Successful in 3m59s
- getAdminUser now properly supports platform_admin with 0 brand links and loads full brand_ids
- createAdminUser action now inserts into admin_user_brands join table
- Admin layout surfaces the signed-in email on Access Denied
- AdminAccessDenied links to /login instead of dead-end /admin
- Main dashboard uses direct pool query instead of dead supabase shim
- Improved provision-admin.ts script for prod bootstrap (loads .env.production too)
2026-06-09 15:30:23 -06:00
openclaw cb0c9c8545 fix(layout): use || instead of ?? so empty SITE_URL falls back to default
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
2026-06-09 12:35:05 -06:00
openclaw 0f9ca2f331 fix(about): convert to server component to avoid server-only conflict
Deploy to route.crispygoat.com / deploy (push) Failing after 3m25s
2026-06-09 12:29:48 -06:00
openclaw 916ad39176 feat(storage): MinIO object storage, Neon Auth, Supabase removal
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject,
  deleteObject, presigned URL helpers, and BUCKETS constant
- Wire product images, brand logos, and water log photos to MinIO via the
  new storage client
- Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call)
- Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge
  Function proxy)
- Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass
  brand_settings.logo_url from all call sites
- Update email templates to use dynamic logoUrl instead of hardcoded Supabase
  bucket URLs
- Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage,
  TimeTrackingFieldClient; use brand_settings props + local public/ fallback
- Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to
  public/ for local development
- Add MinIO env vars to .env.example (endpoint, access key, secret, buckets)
- Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props
- Fix admin/users.ts logoUrl type (null → undefined for optional string)
- Remove stale sb- cookie from wholesale-auth
- Migrate tuxedo/about page to remove supabase import and use pool query for
  wholesale_settings lookup
2026-06-09 12:23:37 -06:00
tyler bb464bda65 fix(products): migrate catalog page from supabase to Drizzle + fix brands→tenants
Deploy to route.crispygoat.com / deploy (push) Successful in 5m12s
The products list page at /admin/products still queried the legacy Supabase
mock client, which returns an empty array. Combined with the SaaS schema
renaming brands → tenants, the platform admin's brand picker for new
products was silently failing (getBrands SELECTed from a non-existent
'brands' table), making 'Add Product to catalog' look like a no-op.

- src/app/admin/products/page.tsx: replace supabase.from('products') with
  a Drizzle query using withPlatformAdmin (cross-tenant) or
  withTenant(brandId, ...) (scoped). Map new columns back to the legacy
  Product shape the existing UI consumes:
    * price_cents (integer) → price (dollars)
    * tenant_id → brand_id
    * product_images LEFT JOIN for first image per product
    * default type='pickup', is_taxable=false (columns not in SaaS schema)
- src/actions/admin/users.ts (getBrands): query tenants table instead of
  the legacy brands table.

Verified locally against the seeded dev DB: 8 products across 2 tenants
now render in the catalog, and the brand picker populates with both
tenants for platform admins.
2026-06-07 07:37:48 +00:00
tyler 03cf2f446f fix(login): only show Google button when client ID looks like a real Google OAuth ID
Deploy to route.crispygoat.com / deploy (push) Successful in 2m51s
Previously, any non-empty AUTH_GOOGLE_ID + AUTH_GOOGLE_SECRET would
trigger the Google button. That broke dev/CI setups where the env
vars are placeholder strings (e.g. 'dummy-google-client-id') — the
button would render and immediately 401 from Google with 'invalid_client'.

Real Google OAuth client IDs always end in '.apps.googleusercontent.com'.
Gate hasGoogle on that suffix so the login page falls back to the
credentials form (or the 'not configured' message) when the values
aren't real.
2026-06-07 07:17:39 +00:00
tyler 50201b00fe migrate: replace Supabase REST with Drizzle/pg in 28 more files (wave 5 final)
- admin components: TaxDashboard, OrderTableBody, TaxQuarterlySummary, StopProductAssignment, StopTableClient, ProductAssignmentForm, StopMessagingForm
- api routes: wholesale/checkout, wholesale/price-sheet, api/supabase (DELETED), api/reports/export, route-trace/* (stubbed - feature retired)
- lib: src/lib/supabase.ts (mock-only shim, no @supabase imports), src/lib/supabase/server.ts (DELETED)
- actions: wholesale-auth (stubbed - awaiting Auth.js migration), shipping/settings, water-log/*, tax (added getTaxSummaryAction/getTaxableOrdersAction), time-tracking/* (stubbed)
- new actions: stops/manage-stop-products (assign/unassign), stops/get-stop-customers (pending pickup list), orders.toggleOrderPickupComplete (legacy column)

All Supabase REST calls replaced with Drizzle ORM, raw pg.Pool queries, or
existing server actions. Typecheck: clean. Tests: 22/22 pass. Build: OK.
2026-06-07 06:24:57 +00:00
tyler 67abcaa2db migrate: replace Supabase REST with Drizzle/pg in 11 more action files (wave 5 partial)
- analytics.ts: rewrite getReportsSummary, getRevenueChart, getSalesByProduct,
  getContactGrowth, getRecentOrders, getConversionFunnel against pool + new
  orders/customers schema. Drops retired columns (subtotal, pickup_complete)
  and re-implements the SQL by hand.
- import-orders.ts: bulk import via withTx using orders + orderItems + customers
  Drizzle tables, computes total_cents from current product prices.
- import-products.ts: rewrite to use withTenant(brandId) and Drizzle products
  table.
- products/create-product.ts, update-product.ts, upload-image.ts: switch to
  withTenant + Drizzle; image_url moves to product_images table.
- reports.ts: rewrite against pool + new orders schema.
- route-trace/lots.ts: stub functions (route-trace feature retired from SaaS
  rebuild — harvest_lots table not in db/schema). Uses discriminated union
  return types so consumer narrowing works in both branches.
- settings/features.ts: switch to withTenant + Drizzle brandSettings.
- shipping.ts: switch to pool + Drizzle orders/orderItems.
- api/v1/referrals/route.ts: fix typecheck (referred_user_id undefined → 'anonymous').

Typecheck: clean. Tests: 22/22 pass. Build: succeeds.
2026-06-07 05:26:03 +00:00
tyler 3ad2a48fc3 migrate: replace Supabase REST with Drizzle/pg in communications + marketing (wave 2 redo) 2026-06-07 03:58:26 +00:00
tyler 99a3d66636 migrate: replace Supabase REST with Drizzle/pg in billing + integrations + wholesale (wave 3) 2026-06-07 03:25:22 +00:00
tyler eb9621d238 migrate: replace Supabase REST with Drizzle/pg in core admin (wave 1) 2026-06-07 03:14:59 +00:00
tyler b8317a200e migrate: replace Supabase REST with Drizzle/pg in water-log/time-tracking/reports/etc (wave 4) 2026-06-07 03:05:00 +00:00
tyler 4b02154188 Merge remote-tracking branch 'crispygoat/main'
Deploy to route.crispygoat.com / deploy (push) Failing after 1m59s
# Conflicts:
#	src/actions/admin/password.ts
#	src/actions/brand-settings.ts
#	src/actions/stops.ts
#	src/app/change-password/page.tsx
#	src/app/login/LoginClient.tsx
#	src/app/logout/page.tsx
#	src/auth.config.ts
#	src/lib/admin-permissions.ts
#	src/lib/auth.ts
#	src/lib/db.ts
2026-06-07 01:56:43 +00:00
tyler 6e71596daf Merge branch 'feature/drizzle-rls-real-auth'
# Conflicts:
#	CLAUDE.md
#	package.json
#	src/app/api/auth/[...nextauth]/route.ts
#	src/app/login/LoginClient.tsx
#	src/auth.config.ts
#	src/components/admin/AdminSidebar.tsx
#	src/lib/admin-permissions-types.ts
#	src/lib/admin-permissions.ts
#	src/lib/auth.ts
#	src/middleware.ts
2026-06-07 01:55:06 +00:00
tyler 4da7aae5ce feat(auth): seed admin user + email/password login UI
- db/seed.ts: upserts admin@route-commerce.local with a scrypt password
  hash (default password 'admin', override with SEED_ADMIN_PASSWORD)
  and grants them the platform_admin role on the Tuxedo tenant so
  getAdminUser() resolves it for the Credentials provider's authorize
  function
- src/app/login/page.tsx: reads ?error=... from the URL and passes
  hasCredentials + seededEmail + error to the client so the form
  pre-fills in dev and surfaces 'Invalid email or password' cleanly
- src/app/login/LoginClient.tsx: adds the email + password form below
  the Google button, with a divider, dev-mode pre-fill, and local error
  handling for client-side failures (server-side failures come back
  through the ?error=... param)
2026-06-07 01:51:53 +00:00
tyler 7cd0603cfb feat: remove dev_session, add Drizzle schema + RLS + real auth
BREAKING: dev_session cookie bypass removed. Admin access now requires
a real Auth.js v5 session (Google OAuth in production). Provision users
by inserting into users + tenant_users tables.

New in this commit:
- db/migrations/0001_init.sql: 18-table SaaS schema with RLS (tenants,
  users, tenant_users, plans, add_ons, subscriptions, tenant_add_ons,
  products, product_images, stops, customers, orders, order_items,
  brand_settings, email_templates, campaigns, files, audit_log)
- db/schema/: Drizzle TypeScript mirror of every table
- db/client.ts: withTenant() / withPlatformAdmin() query wrappers that
  set Postgres GUCs (app.current_tenant_id, app.platform_admin) for
  RLS enforcement. Never query a tenant-scoped table without one.
- db/seed.ts: seeds 3 plans, 6 add-ons, 2 tenants (Tuxedo, Indian River
  Direct), brand_settings, sample products/stops/customers
- scripts/migrate.js: applies migrations in lexical order with tracking
- scripts/db-reset.js: drops + recreates DB, runs migrate + seed
- DATABASE_URL now uses rc_app (non-superuser, NOBYPASSRLS). RLS is
  enforced even for the app user. DATABASE_ADMIN_URL for migrations.
- src/lib/admin-permissions.ts: getAdminUser() reads Auth.js session,
  looks up user + tenant in Postgres. brand_id kept as alias for
  backward compat.
- src/middleware.ts: Auth.js-only route protection, dev_session gone
- src/app/login/LoginClient.tsx: Google OAuth only, no demo mode
- src/components/admin/AdminSidebar.tsx + AdminHeader.tsx: signOutAction
  replaces supabase signout
- @/db/* path aliases in tsconfig.json + vitest.config.ts
- drizzle.config.ts added
- db/auth_schema.sql removed (was a stub; replaced by real schema)
- src/app/api/dev-login/route.ts deleted
- tests: updated to remove dev_session coverage
2026-06-07 01:23:44 +00:00
tyler f96dcd01f2 feat(db+auth): add pg pool, admin_users email/provider migration, refactor auth lookup
- Create src/lib/db.ts (shared pg.Pool, query/withTx helpers, server-only)
- Add @types/pg devDep
- Migration 204: add email, auth_provider, auth_subject columns to
  admin_users; backfill from auth.users; new upsert_admin_user accepts
  multi-provider args; new get_admin_user_for_session RPC resolves
  Auth.js session id (UUID or Google sub) to an admin row
- Refactor getAdminUser() to use pg + new RPC; auto-provisions on first
  Google sign-in using session.user.email
- Refactor updatePasswordAction to call update_user_password via pg
  (drops the Supabase REST hop)
- Delete orphaned src/actions/login.ts (replaced by auth-actions.ts)
- Drop remaining DEV_FORCE_UID references in users.ts; dev path now
  uses dev_session cookie (the only cookie the dev flow can set)
- Update AdminUser type: user_id is now string | null (Google users
  have no Supabase auth id); add email + auth_provider fields
- Fix downstream type errors: StopProductAssignment.callerUid accepts
  null; pickup.ts performedBy widens to string | null
- Bump vitest config, tests/, and other earlier cleanup changes
2026-06-06 23:41:41 +00:00
tyler 3f731f7739 fix(admin): wrap post-auth data loads in try/catch to surface and survive SC render errors
Deploy to route.crispygoat.com / deploy (push) Successful in 3m9s
The /admin page was throwing a Server Components render error (digest
4266906817) in production. The exact throw point was not visible from
the browser (Next.js hides it for security) and server logs were not
available at debugging time.

The layout and page both call data-loading functions (getActiveBrandId,
listBrandsForAdmin, supabase brands lookup) that could throw on a
transient DB/network failure, and these calls had no try/catch. A
single failed call would crash the entire admin shell.

Wrap each call in try/catch with console.error so:
  1. The page renders with sensible defaults if a call fails
  2. The actual error is logged server-side (visible via the digest in
     the admin error boundary or PM2/Docker logs)
  3. The admin shell stays functional even if a single data source is
     down

No behavior change on the happy path.
2026-06-06 22:35:24 +00:00
tyler 5654ebaecd chore(auth): remove legacy rc_auth_uid/rc_uid/upsert_admin_user path; scope sign-in to ADMIN_ALLOWED_EMAILS; silence lockfile warning
Deploy to route.crispygoat.com / deploy (push) Successful in 3m14s
Cleanup after Auth.js v5 became the only sign-in path. The platform
had three overlapping auth modes (dev cookie, legacy rc_auth_uid, Auth.js
JWT) and a pile of dead-code pages/routes that only existed to support
the legacy path.

What changed:

* getAdminUser() now has only two auth paths:
    1. dev_session cookie (auto-issued by src/proxy.ts for /admin/* when
       ALLOW_DEV_LOGIN is enabled)
    2. Auth.js v5 JWT (the encrypted cookie + auth() lookup)
  The legacy rc_auth_uid/rc_uid branch and the Supabase REST fetch
  against admin_users are gone.

* The signIn callback in src/lib/auth.ts enforces ADMIN_ALLOWED_EMAILS
  when set. Unset = open mode (backward compatible with demo/dev). Dev
  credentials provider is exempt. The new env var is wired through
  .env.example and .gitea/workflows/deploy.yml (read from
  secrets.ADMIN_ALLOWED_EMAILS, written to the server .env file).

* change-password/page.tsx now uses auth() server-side instead of
  fetching the deleted /api/auth/uid endpoint. The form is split into
  page.tsx (server component, auth check) + ChangePasswordForm.tsx
  (client component, form state). updatePasswordAction now reads the
  user id from auth() instead of the rc_auth_uid cookie.

* Deleted 14 dead-code files:
    - Pages: login2, logout, auth/callback, admin/debug-auth,
      admin/test-auth
    - API routes: api/login, api/logout, api/auth/uid, api/force-admin,
      api/set-auth-cookie, api/debug-cookie, api/debug-me,
      api/debug-auth
    - Actions: src/actions/login.ts
  These were the old email/password login, the old Supabase OAuth
  callback, the old /api/auth/uid probe, and a pile of debug endpoints
  that have been superseded by the new proxy + the new /login page.

* next.config.ts: set outputFileTracingRoot: '.' to silence the
  Next.js 16 lockfile-inference warning. Without this the build
  walked up from package.json looking for a lockfile, found the
  homelab runner's stale act cache at /home/tyler/.cache/act/.../package-lock.json,
  and warned on every build. '. resolves to the project root in both
  dev and CI, so it's the right answer.

Out of scope (deferred):

* src/actions/admin/users.ts still uses rc_auth_uid internally for its
  dev-bypass logic. It works (the rc_auth_uid branch is gated on
  NODE_ENV != 'production' and DEV_FORCE_UID), but it's now genuinely
  unreachable in production. Clean up in a follow-up.

Pre-flight:
* npx tsc --noEmit: clean
* npm run lint (touched files): clean
* npm run build: clean — proxy picked up, no lockfile warning, all
  93 static pages generated.
2026-06-06 22:13:56 +00:00