Commit Graph

169 Commits

Author SHA1 Message Date
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 3f323dd52a merge: wave-2-redo branch (communications + marketing migration) 2026-06-07 03:59:49 +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 01198111ea fix(build): remove duplicate proxy.ts (Next.js 16 uses middleware)
Next.js 16 enforces 'one of middleware.ts OR proxy.ts' - both can't
coexist. Our src/middleware.ts (Auth.js v5) is the canonical one;
src/proxy.ts was the legacy dev_session auto-issuer.
2026-06-07 02:08:37 +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 ccfd89be75 feat(auth): wire Credentials provider + edge-safe authConfig
- src/auth.config.ts: edge-safe Auth.js config (no pg, no Credentials)
  imported by the middleware; exports authConfig + isDevLoginEnabled
- src/lib/auth.ts: extends authConfig with the Credentials provider
  (Node-only) for email + password sign-in via users.password_hash
- src/middleware.ts: uses authConfig (edge-safe) instead of the full
  src/lib/auth.ts so the edge bundle stays small
- src/actions/auth-actions.ts: adds signInWithCredentials that calls
  the Credentials provider; signInWithGoogle stays the production path
2026-06-07 01:50:53 +00:00
tyler 42f28b32a4 test(passwords): add unit tests for encode/verify/round-trip 2026-06-07 01:46:52 +00:00
tyler 720ffe5262 feat(auth): add password_hash column + bcrypt helper for Auth.js Credentials provider
- Migration 0002 adds nullable password_hash to users (idempotent)
- src/lib/passwords.ts: encode/verify with self-describing format
  (algo$N$salt$hash) so we can migrate to a stronger KDF later
- Schema adds passwordHash column; OAuth-only users leave it null
2026-06-07 01:36:27 +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
tyler 1cecbce392 fix(auth): port dev auto-login to Next.js 16 proxy.ts
Deploy to route.crispygoat.com / deploy (push) Successful in 3m8s
Next.js 16 renamed middleware.ts to proxy.ts and only allows one of the
two. Delete src/middleware.ts and rewrite src/proxy.ts to do the dev
auto-login + /login bounce + /admin gate explicitly (no NextAuth auth()
wrapper — auth.handler() returns a NextMiddleware taking (req, event),
which makes wrapping it awkward when we also need to set cookies on the
response).

Drop the now-dead 'authorized' callback from auth.config.ts (it was only
fired by the NextAuth wrapper, which we no longer use).

Migration 209: add a defensive unique constraint on admin_users.user_id
so the ON CONFLICT (user_id) clause in the new RPC resolves regardless
of whether the Supabase-dashboard-created table shipped with one.
2026-06-06 21:52:19 +00:00
tyler b63d0415ab fix(deploy): seed docker-compose.yml before any docker compose cmd
Deploy to route.crispygoat.com / deploy (push) Failing after 1m7s
The 'cp -f deploy/docker-compose.yml ...' was sitting AFTER
'docker compose down' in the step. 'docker compose down' reads
and validates the compose file on the server — so the old copy
(with the dead nextjs service and its env_file: ../.env.production
reference) was still being read, and docker compose bailed on the
missing .env.production file before the copy could overwrite it.

Moved the config-file seeding to the top of the step, right after
'mkdir -p $APP_DIR'. Now the new compose file is in place before
either 'docker compose down' or 'docker compose up' runs.
2026-06-06 21:29:21 +00:00
tyler e2e56252ec fix(deploy): always copy docker-compose.yml to server
Deploy to route.crispygoat.com / deploy (push) Failing after 3s
The 'Start Docker stack' step used '[ -f ... ] || cp' for
docker-compose.yml, which only copied the file on the first
deploy. Subsequent deploys kept the stale copy on the server.

The stale copy still had the dead 'nextjs' service with
'env_file: ../.env.production', which docker compose validates
on every 'up' and bailed because .env.production is written
later by the 'Deploy' step.

Changed to unconditional 'cp -f' so the server always has the
latest compose file.
2026-06-06 20:48:43 +00:00
tyler 48ce5665b9 docs(memory): deploy fix notes
Deploy to route.crispygoat.com / deploy (push) Failing after 3s
2026-06-06 20:47:15 +00:00
tyler 2d55791458 fix(deploy): PostgREST env + remove dead nextjs service from compose
Deploy to route.crispygoat.com / deploy (push) Failing after 3s
Build was failing on the 'Start Docker stack' step with two issues:

1. PGRST_DB_URI not set — the env var was only in the 'Deploy' step,
   which runs after PostgREST has already started. PostgREST booted
   with a blank DB URI and the step exited 1.

2. docker-compose.yml had a 'nextjs' service with
   env_file: ../.env.production, but .env.production is written
   later by the 'Deploy' step. docker compose validates the entire
   compose file on 'up' and bailed because the path didn't exist
   yet.

   The 'nextjs' service is dead code anyway: PM2 runs Next.js
   directly from $APP_DIR, never through docker. Removed it.

Also fixed: 'docker compose up -d db postgrest minio minio_init'
referenced services that don't exist in the compose file (Postgres
runs on the host, not in docker). Changed to just 'postgrest', and
the pg_isready check now uses host psql directly instead of
'docker compose exec -T db'.

Changes:
- deploy/docker-compose.yml: drop nextjs service, keep only postgrest
- .gitea/workflows/deploy.yml:
  - Add PGRST_DB_URI / PGRST_DB_ANON_ROLE / PGRST_SERVER_PORT to
    the 'Start Docker stack' step env
  - Write them to $APP_DIR/.env so docker compose picks them up
  - 'docker compose up -d postgrest' (was: db postgrest minio minio_init)
  - pg_isready check uses host psql (was: docker compose exec -T db)
2026-06-06 20:46:52 +00:00
tyler 6c5ca6829f docs(memory): Auth.js v5 wiring notes
Deploy to route.crispygoat.com / deploy (push) Failing after 3s
2026-06-06 20:31:13 +00:00
tyler 1e9f9c0414 feat(auth): wire getAdminUser() to Auth.js v5 Google sign-in
Deploy to route.crispygoat.com / deploy (push) Failing after 2s
After a user signs in with Google, they land on /admin but see
'Your account does not have admin access' because getAdminUser()
only checked the legacy dev_session and rc_auth_uid cookies.

This completes the Auth.js path:

- New src/lib/db.ts: shared pg.Pool singleton (extracted from
  src/lib/auth.ts). The single connection pool for the whole app
  — server actions, API routes, and Auth.js all import from here.

- src/lib/auth.ts: imports the shared pool, signIn event now calls
  the new upsert_admin_user_for_authjs RPC (idempotent) to
  auto-create a platform_admin row on first sign-in.

- New supabase/migrations/209_authjs_auto_create_admin.sql:
  - Defensive ALTER TABLE ADD COLUMN IF NOT EXISTS for
    can_manage_settings (was likely dashboard-added, not in any
    tracked migration)
  - SECURITY DEFINER RPC upsert_admin_user_for_authjs(p_user_id UUID)
    that inserts a platform_admin row with all permissions true
    and ON CONFLICT (user_id) DO NOTHING
  - NOTIFY pgrst to reload PostgREST schema cache

- src/lib/admin-permissions.ts: new Auth.js session check between
  dev_session and rc_auth_uid. Uses auth() from @/lib/auth to
  decrypt the JWT cookie server-side, then getAdminUserFromPool()
  queries admin_users + admin_user_brands via the shared pool.
  Legacy rc_auth_uid path unchanged (deferred).

- src/middleware.ts: recognizes Auth.js session cookies
  (authjs.session-token and __Secure-authjs.session-token) at the
  edge so signed-in users aren't bounced to /login.

Flow after this change:
  Dev/demo:  visit /admin → middleware auto-issues dev_session → in
  Prod:      click Google → Auth.js OAuth → signIn event creates
             admin_users row → redirect to /admin → getAdminUser()
             reads JWT, queries pool, returns platform_admin.
2026-06-06 20:30:11 +00:00
tyler 53d995fc99 docs(memory): login flow consolidation notes
Deploy to route.crispygoat.com / deploy (push) Failing after 3s
2026-06-06 20:14:44 +00:00
tyler e499139c74 fix(login): one-button Google sign-in + middleware auto-issues dev cookie
Deploy to route.crispygoat.com / deploy (push) Failing after 3s
The dev login flow was redirecting back to /login because:
  - src/middleware.ts didn't exist, so the Auth.js authorized
    callback in auth.config.ts never ran
  - Even if it had, it only checked the Auth.js JWT, not dev_session
  - Clicking the demo buttons set the cookie via document.cookie,
    but the admin layout (via getAdminUser) was the only thing
    reading it — no edge gate

Fix:
  - New src/middleware.ts: gates /admin/* and /login at the edge.
    Auto-issues dev_session=platform_admin when ALLOW_DEV_LOGIN is
    enabled (default on, set to 'false' in prod). No buttons, no
    client-side cookie games.
  - LoginClient.tsx: stripped to a single Google OAuth button.
    Removed email/password form, dev credentials form, and the
    /login?demo=1 three-button picker.
  - Removed signInWithDev from auth-signin.ts (no longer called).
  - Removed dead /dev-login page and /api/dev-login route.

Net result: one sign-in path (Google), invisible dev auto-login
via middleware, no more three modes.
2026-06-06 20:14:08 +00:00
tyler 7489da3da0 docs(memory): record successful build + production prep checklist
Deploy to route.crispygoat.com / deploy (push) Failing after 3s
2026-06-06 19:58:26 +00:00
tyler 32396af193 ci(gitea): fix deploy path — docker-compose.yml moved to deploy/
Deploy to route.crispygoat.com / deploy (push) Successful in 3m30s
The Start Docker stack and Deploy steps referenced docker-compose.yml at
the repo root, but the file moved to deploy/ as part of the deploy.sh
refactor. The cp commands failed with 'cannot stat docker-compose.yml'
and the deploy step aborted.

Updated both cp paths to deploy/docker-compose.yml. The cd $APP_DIR and
docker compose -f $APP_DIR/docker-compose.yml references are unchanged
because they point at the deployed copy in APP_DIR, not the repo.
v1.0.0-build-fixed
2026-06-06 19:54:03 +00:00
tyler f36419be69 docs: document canonical Gitea remote in CLAUDE.md
Deploy to route.crispygoat.com / deploy (push) Has been cancelled
There is exactly one remote (origin = git@git.crispygoat.com:tyler/route-commerce.git).
No GitHub remotes. Pushing to origin/main triggers .gitea/workflows/deploy.yml.
2026-06-06 19:49:46 +00:00
tyler 5477b3419f fix(build): make admin tree dynamic and catch Supabase fetch errors at build
Deploy to route.crispygoat.com / deploy (push) Failing after 4m16s
Two errors were aborting the Gitea build:

1. DYNAMIC_SERVER_USAGE on /admin/settings/square-sync (and any admin page):
   getAdminUser() reads cookies() via next/headers. The admin layout tried
   to prerender statically, so the first child page that hit cookies()
   aborted the build. Added 'export const dynamic = "force-dynamic"' to
   src/app/admin/layout.tsx so the whole admin tree opts out of static
   prerender.

2. Prerender ECONNREFUSED on /indian-river-direct/stops and the sitemap:
   getPublicStopsForBrand / getActiveStopsForSitemap / getBrandSettingsPublic
   fetch NEXT_PUBLIC_SUPABASE_URL at build time. The Gitea runner sets the
   Supabase env vars (so the existing env-var guard passes) but the URL
   is unreachable, so fetch throws ECONNREFUSED and the prerender aborts.
   Wrapped each fetch in try/catch returning [] / {success: false} so the
   prerender completes; runtime behavior is unchanged when the fetch
   succeeds.

Also added force-dynamic to the square-sync page itself as belt-and-braces
in case the layout change doesn't propagate.
2026-06-06 19:46:35 +00:00
tyler 2f3be5426f fix(actions): skip Supabase fetch at build time when env vars unset
Deploy to route.crispygoat.com / deploy (push) Failing after 2m50s
The /indian-river-direct/stops page and sitemap prerender at build time
and call getPublicStopsForBrand / getActiveStopsForSitemap / getBrandSettingsPublic.
Those actions fetch NEXT_PUBLIC_SUPABASE_URL via Supabase REST. During
the GitHub/Gitea build, the Supabase secret is unset (or the value is
".supabase.co" which doesn't resolve), so the fetch errors with
ECONNREFUSED and the build aborts.

Return [] / not-configured when the env vars are missing so the prerender
can complete. Runtime behavior is unchanged when the vars are set.
2026-06-06 05:12:55 +00:00
tyler 2d837bc786 ci(gitea): drop build workflow, simplify deploy to call deploy.sh
Deploy to route.crispygoat.com / deploy (push) Failing after 2m50s
- Delete .gitea/workflows/build.yml (typecheck/lint only; caused confusion)
- Rewrite .gitea/workflows/deploy.yml as a thin wrapper that calls
  ./deploy/deploy.sh, matching the design in deploy/GITEA_SETUP.md
  (Option B). The 14512-byte inline deploy is removed; deploy.sh is the
  source of truth for the deploy mechanism.
- Fix runs-on to [self-hosted, ubuntu-latest] (matches the actual labels
  registered on crispygoat-host-runner; the previous [.., linux, ..] was
  unmatchable, which is why runs were stuck in the queue)
2026-06-06 05:03:44 +00:00
tyler bb6dbe37a4 ci(gitea): add deploy workflow + self-hosted homelab deploy toolkit (Auth.js port)
Build / build (push) Has been cancelled
Deploy to route.crispygoat.com / deploy (push) Has been cancelled
Ports the deploy pipeline from the Gitea main fork (commit 7ddb06d's deploy
toolkit) into the Auth.js v5 / NextAuth tree:

- .gitea/workflows/deploy.yml: inline deploy that brings up the Docker
  stack, applies migrations, builds Next.js, and runs the app under PM2.
  Swapped Better Auth env vars (BETTER_AUTH_SECRET/URL, NEXT_PUBLIC_BETTER_AUTH_URL)
  for Auth.js v5 names (AUTH_SECRET/URL, NEXT_PUBLIC_AUTH_URL). Dropped
  NEXT_PUBLIC_SUPABASE_URL/ANON_KEY (Supabase removal in progress). Added
  GOOGLE_CLIENT_ID/SECRET + ALLOW_DEV_LOGIN for the Auth.js Google provider
  and dev credentials path. Switched runs-on from 'ubuntu-latest' to the
  self-hosted runner labels matching build.yml.

- deploy/: idempotent deploy toolkit (deploy.sh, docker-compose.yml,
  Dockerfile.nextjs, nginx.conf.template, .env.production.example, healthcheck.sh,
  Makefile, deploy/.gitignore). No auth/Supabase dependencies — pure infra.

- deploy/.env.production.example: renamed NEXTAUTH_SECRET/NEXTAUTH_URL
  (v4) to AUTH_SECRET/AUTH_URL (v5) and added the v5-specific vars
  (NEXT_PUBLIC_AUTH_URL, GOOGLE_*, ALLOW_DEV_LOGIN).

Build pipeline is now end-to-end:
  build.yml → typecheck + lint + build (uses [self-hosted, linux, ubuntu-latest])
  deploy.yml → start docker stack + migrations + build + PM2 restart

Storage / admin code ports (MinIO via @/lib/storage, Supabase removal,
admin-permissions rewrite) are tracked separately — they require porting
the storage and admin code first; the deploy pipeline itself is ready
to run against the Auth.js world.
2026-06-06 04:24:53 +00:00
tyler 3f4f46da7e ci: retrigger Gitea build 2026-06-06 03:46:49 +00:00
tyler ec1506dc82 feat(auth): Auth.js v5 + Postgres adapter for local smoke test
Wire up NextAuth v5 with @auth/pg-adapter, JWT sessions (edge-friendly),
and a dev Credentials provider for local testing without Google OAuth.

Stack
- next-auth@5.0.0-beta.31, @auth/pg-adapter@1.11.2, @types/pg
- Google OAuth provider via GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET
  (falls back to AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET)
- Postgres adapter wired to a single pg.Pool in src/lib/db.ts style —
  reads DATABASE_URL with SUPABASE_DB_URL / POSTGRES_URL fallbacks
- JWT session strategy (edge-safe) so the proxy can verify sessions
  without a DB round-trip

Files
- src/auth.config.ts        edge-safe config (Google + authorized cb)
- src/lib/auth.ts           server config (adapter + dev Credentials)
- src/proxy.ts              Next.js 16 proxy (was middleware.ts)
- src/app/api/auth/[...nextauth]/route.ts
                            catch-all handler
- src/app/protected-example/page.tsx
                            demo page that renders auth() session
- src/actions/auth-signin.ts
                            signInWithGoogle, signInWithDev,
                            signOutAction server actions
- src/app/login/LoginClient.tsx
                            added "Sign in with Google" + dev form
- supabase/migrations/204_authjs_tables.sql
                            users / accounts / sessions /
                            verification_token schema (UUID-keyed)
- .env.example              AUTH_SECRET, AUTH_URL, GOOGLE_CLIENT_*,
                            DATABASE_URL, ALLOW_DEV_LOGIN

Removed
- src/middleware.ts         deleted; Next.js 16 only runs one proxy
                            (the new src/proxy.ts is canonical)

Routes
- /login, /admin, /admin/*, /protected-example
                            proxy matcher
- /api/auth/{providers,csrf,signin/<provider>,callback/<provider>,
  session,signout}
                            standard Auth.js endpoints

Local dev
- npm run dev (now runs on port 4000)
- push migration 204 then visit /login
- dev signin works with any non-empty username/password
  (hidden when ALLOW_DEV_LOGIN=false)
- Google signin requires real GOOGLE_CLIENT_ID + redirect URI
  http://localhost:4000/api/auth/callback/google

Verified
- tsc --noEmit clean
- /admin, /admin/orders, /protected-example → 307 to /login
  when unauthenticated
- /api/auth/session returns user after signin
- /protected-example renders session info
- /api/auth/providers returns google + dev-login

Docs
- CLAUDE.md and MEMORY.md updated to reflect the Supabase → Postgres
  + Auth.js v5 pivot

Gradual migration in progress
- src/lib/admin-permissions.ts still uses dev_session / rc_auth_uid;
  the admin shell will show 'Access Denied' for Auth.js-only
  sessions until each page is flipped over
- @supabase/* packages remain in package.json for the same reason
- production deployment (AUTH_URL=https://, __Secure- cookies) is
  out of scope for this pass
2026-06-06 03:40:09 +00:00
tyler 2b3fd214d8 ci(gitea): add build workflow for self-hosted runner
Runs typecheck, lint, and build on every push to main and on pull requests.
Targets the self-hosted Gitea Actions runner (crispygoat-host-runner)
using labels [self-hosted, linux, ubuntu-latest].

Uses the local dev stack endpoints (PostgREST on :3001, MinIO on :9000,
Postgres on :5432) for env vars so no secrets need to be wired in.
2026-06-05 23:00:10 +00:00
tyler 858ca0d64d test(scripts): add end-to-end validation script for local Postgres + PostgREST + MinIO + Next.js stack
Validates the full local development stack:
- Postgres connectivity
- PostgREST API on :3001
- MinIO object storage on :9000
- Next.js dev server on :4000
- Brand isolation via SECURITY DEFINER RPCs
- Auth via dev_session cookie (3 roles)
2026-06-05 22:49:52 +00:00
tyler 8a91494009 fix: add both apikey and Authorization headers for storage upload 2026-06-04 20:31:58 +00:00
tyler b240b7b56e fix: remove redundant Authorization header from storage upload 2026-06-04 20:31:38 +00:00
tyler fd9d6424d5 fix: add available_from/available_until to Product type in ProductsClient 2026-06-04 20:26:10 +00:00
tyler 0b748adfaa Add seasonal availability dates to product form modal
- Add available_from and available_until fields to ProductFormValues type
- Add date picker inputs for seasonal availability in ProductFormModal
- Wire availability dates through ProductsClient to modal initial values
- Create migration 149 for database columns (already applied)
2026-06-04 20:20:09 +00:00
tyler 69767eb250 feat(admin): redesign product form modal (Atelier des Récoltes)
Replace the basic add/edit product modal with an editorial two-column
layout featuring Fraunces display serif, IBM Plex Mono section labels,
a custom drop zone, visual type cards, and pill toggles. Add a brand
selector visible only to platform admins to fix the 'Brand ID is
required' / RLS error when creating products without an assigned brand.

- Add ProductFormModal with two-column layout (image hero + form)
- Add Fraunces / IBM Plex Mono / Inter Tight via next/font/google
- Add atelier-* design system to globals.css
- Wire brand picker through page server component -> client
- Convert ProductsClient image handling to callback shape
- Move all submit/validation/upload logic into the new modal
2026-06-04 19:54:20 +00:00
tyler 8bca9e86b1 chore: add Tuxedo Corn 2026 tour schedule 2026-06-04 19:17:39 +00:00
tyler 82d81b2f69 fix(stops): change stops.date from TIMESTAMPTZ to DATE
The stops.date column was created as TIMESTAMPTZ but is used as a
calendar date throughout the app. Supabase returned it as a
'YYYY-MM-DD HH:MM:SS+TZ' string, which made the existing
'new Date(s.date + "T00:00:00")' parsing in the client produce
Invalid Date objects — leaving the Stops & Routes calendar empty.

Changing the column to DATE makes Supabase return a clean 'YYYY-MM-DD'
string that the existing local-time parsing handles correctly.

- ALTER stops.date TYPE DATE USING date::DATE
- Recreate get_public_stops_for_brand with date DATE in RETURNS TABLE
  (DROP + CREATE required because CREATE OR REPLACE refuses to change
  the OUT-parameter row type — PostgreSQL error 42P13)

The time (TEXT 'HH:MM') and cutoff_time (TIMESTAMPTZ) columns are
unchanged — they legitimately carry time-of-day information.
2026-06-04 18:56:20 +00:00
tyler 7ecb1f2fc0 Merge branch 'main' of github.com:dzinesco/route-commerce
# Conflicts:
#	src/app/admin/stops/page.tsx
#	src/app/globals.css
#	src/app/layout.tsx
2026-06-04 18:43:58 +00:00
tyler 015b1cf7b5 feat(checkout): real Stripe Express + Elements on /checkout
- Add @stripe/stripe-js + @stripe/react-stripe-js
- New src/lib/stripe-client.ts: cached loadStripe helper
- New src/actions/billing/retail-payment-intent.ts: server action
  that creates a PaymentIntent with automatic_payment_methods
- New src/components/storefront/StripeExpressCheckout.tsx: embedded
  ExpressCheckoutElement (Apple Pay, Google Pay, Link, PayPal) +
  PaymentElement (card) + hosted-checkout fallback
- /checkout form is now controlled; StripeExpressCheckout reads
  name/email/stop from form state and confirms in-page via
  stripe.confirmPayment
- /checkout/success handles both ?session_id= and ?payment_intent=
  so embedded + hosted flows both land on the same order-creation
  page using the pending_checkout sessionStorage payload
- Export StopInfo from CartContext and select 'time' on the stops
  fetch so the local Stop shape matches the context's StopInfo
- QuickCartSheet express buttons are visual shortcuts only;
  /checkout auto-renders the real Apple Pay / Google Pay buttons

Requires NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY for the embedded path;
if missing the embedded section falls back to the hosted
Stripe Checkout button. npx tsc --noEmit passes.
2026-06-04 18:38:21 +00:00
tyler 1b12a0a95d feat(admin/stops): add tabbed dashboard with calendar and locations views
Replace the table-only stops view with a three-tab 'Harvest Dispatch'
almanac dashboard.

- Calendar tab: month grid with stops as status-colored pins, day-detail
  side panel, prev/next/today nav, weekend tint, sunrise gradient on
  the current day.
- Locations tab: cards grouped by city+state with per-location stats
  (stops/active/upcoming), date range, next-stop ribbon, and inline
  expansion to list every stop at that location. Top of the tab shows
  a six-card almanac stats strip (Locations/Cities/Stops/Active/Upcoming/
  Drafts).
- List tab: condensed read-only list of all stops in date order.

Typography: load Fraunces (display), Manrope (body), and Fragment Mono
(mono) via next/font/google, wired into the Tailwind theme as
--font-display, --font-sans, --font-mono. Add typescript to devDeps
for tsc --noEmit in this environment.

Visual signature: cream parchment cards, forest green + clay accents,
Roman-numeral stat cells, paper-grain noise, binder-tab nav, route
number watermarks on location cards, harvest-pin SVG markers.

Verified with tsc --noEmit, eslint on the new files, and a 200 from
GET /admin/stops in the dev server.
2026-06-04 18:20:11 +00:00
tyler b2aa53f274 feat(storefront): dedicated Sweet Corn Box product page with 2-3 click order flow
- New route /tuxedo/products/sweet-corn-box with editorial Fraunces/JetBrains
  Mono typography, paper-grain hero, tilted 'Peak Season 2026' stamp, and
  sticky buy-box (desktop) + sticky mobile bottom bar with 48px+ tap targets
- QuickCartSheet slide-up drawer: mobile bottom sheet (spring + drag-to-close)
  vs desktop right drawer, with Apple Pay / Google Pay / Shop Pay express row
  and one-page 'Checkout' CTA — enables the user's specified 2-3 click flow
- CartContext.buyNow() — single-item cart replace + clear stop for 1-tap
  'Quick Buy' that pushes directly to /checkout
- 4-feature check list, 100% Sweetness Guarantee, FAQ accordion, dark final
  CTA strip, and proper SEO/Open Graph meta
2026-06-04 18:01:16 +00:00
tyler 66c6f45efc fix(admin/stops): pass admin user_id, not stop.id, as callerUid
StopDetailModal was hardcoding callerUid={stop.id} when rendering
StopProductAssignment, so assign_product_to_stop received the stop's
own UUID as p_caller_uid. The RPC's admin_users.user_id lookup
returned no row, so every assign attempt failed with
'Not recognized as admin'.

The admin's user_id is already known inside the getStopDetails
server action (it gates on getAdminUser()), so surface it on the
response and read it in the modal. The sibling /admin/stops/[id]
page already uses the same source (adminUser.user_id) — this brings
the modal in line with it.
2026-06-04 17:37:17 +00:00