Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32396af193 | |||
| f36419be69 | |||
| 5477b3419f | |||
| 2f3be5426f | |||
| 2d837bc786 |
@@ -1,52 +0,0 @@
|
|||||||
name: Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: [self-hosted, linux, ubuntu-latest]
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '22'
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Apply fix-agents.js patch
|
|
||||||
run: node fix-agents.js
|
|
||||||
|
|
||||||
- name: Typecheck
|
|
||||||
env:
|
|
||||||
NEXT_PUBLIC_API_URL: http://localhost:3001
|
|
||||||
NEXT_PUBLIC_STORAGE_BASE_URL: http://localhost:9000
|
|
||||||
STORAGE_ENDPOINT: http://localhost:9000
|
|
||||||
DATABASE_URL: postgresql://routecommerce:routecommerce_dev_password@127.0.0.1:5432/route_commerce
|
|
||||||
run: npm run type-check
|
|
||||||
|
|
||||||
- name: Lint
|
|
||||||
env:
|
|
||||||
NEXT_PUBLIC_API_URL: http://localhost:3001
|
|
||||||
NEXT_PUBLIC_STORAGE_BASE_URL: http://localhost:9000
|
|
||||||
STORAGE_ENDPOINT: http://localhost:9000
|
|
||||||
DATABASE_URL: postgresql://routecommerce:routecommerce_dev_password@127.0.0.1:5432/route_commerce
|
|
||||||
run: npm run lint
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
env:
|
|
||||||
NEXT_PUBLIC_API_URL: http://localhost:3001
|
|
||||||
NEXT_PUBLIC_STORAGE_BASE_URL: http://localhost:9000
|
|
||||||
STORAGE_ENDPOINT: http://localhost:9000
|
|
||||||
DATABASE_URL: postgresql://routecommerce:routecommerce_dev_password@127.0.0.1:5432/route_commerce
|
|
||||||
run: npm run build
|
|
||||||
+52
-42
@@ -7,9 +7,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
# Self-hosted Gitea runner (matches build.yml). The Gitea Actions runner
|
runs-on: ubuntu-latest
|
||||||
# is registered with the [self-hosted, linux, ubuntu-latest] labels.
|
|
||||||
runs-on: [self-hosted, linux, ubuntu-latest]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -83,7 +81,7 @@ jobs:
|
|||||||
|
|
||||||
# Seed config files into APP_DIR if missing (they live in the repo, not in APP_DIR)
|
# Seed config files into APP_DIR if missing (they live in the repo, not in APP_DIR)
|
||||||
[ -f $APP_DIR/.env.example ] || cp .env.example $APP_DIR/.env.example
|
[ -f $APP_DIR/.env.example ] || cp .env.example $APP_DIR/.env.example
|
||||||
[ -f $APP_DIR/docker-compose.yml ] || cp docker-compose.yml $APP_DIR/docker-compose.yml
|
[ -f $APP_DIR/docker-compose.yml ] || cp deploy/docker-compose.yml $APP_DIR/docker-compose.yml
|
||||||
cd $APP_DIR
|
cd $APP_DIR
|
||||||
[ -f .env ] || cp .env.example .env
|
[ -f .env ] || cp .env.example .env
|
||||||
# Append production secrets to .env (overriding .env.example defaults)
|
# Append production secrets to .env (overriding .env.example defaults)
|
||||||
@@ -93,7 +91,7 @@ jobs:
|
|||||||
echo "POSTGRES_DB=${POSTGRES_DB}"
|
echo "POSTGRES_DB=${POSTGRES_DB}"
|
||||||
echo "MINIO_ROOT_USER=${MINIO_ROOT_USER}"
|
echo "MINIO_ROOT_USER=${MINIO_ROOT_USER}"
|
||||||
echo "MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}"
|
echo "MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}"
|
||||||
echo "POSTGREST_JWT_SECRET=${POSTGRES_JWT_SECRET}"
|
echo "POSTGREST_JWT_SECRET=${POSTGREST_JWT_SECRET}"
|
||||||
echo "POSTGREST_HOST_PORT=$POSTGREST_HOST_PORT"
|
echo "POSTGREST_HOST_PORT=$POSTGREST_HOST_PORT"
|
||||||
echo "NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL"
|
echo "NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL"
|
||||||
} >> .env
|
} >> .env
|
||||||
@@ -120,11 +118,16 @@ jobs:
|
|||||||
# we need it here for migrations)
|
# we need it here for migrations)
|
||||||
[ -d $APP_DIR/supabase ] || cp -r supabase $APP_DIR/supabase
|
[ -d $APP_DIR/supabase ] || cp -r supabase $APP_DIR/supabase
|
||||||
cd $APP_DIR
|
cd $APP_DIR
|
||||||
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h 127.0.0.1 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -v ON_ERROR_STOP=0 -f supabase/migrations/000_preflight_supabase_compat.sql || true
|
# PAGER= prevents psql from launching less/more in a non-interactive shell,
|
||||||
[ -f supabase/captured_schema.sql ] && PGPASSWORD="${POSTGRES_PASSWORD}" psql -h 127.0.0.1 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -v ON_ERROR_STOP=0 -f supabase/captured_schema.sql || echo "captured_schema.sql not present, skipping"
|
# which hangs indefinitely waiting for keypress. Batch all files into one
|
||||||
for f in supabase/migrations/[0-9]*.sql; do
|
# connection for speed instead of one psql invocation per file.
|
||||||
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h 127.0.0.1 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -v ON_ERROR_STOP=0 -q -f "$f" || echo "FAIL: $f"
|
export PAGER=
|
||||||
done
|
export PGPASSWORD="${POSTGRES_PASSWORD}"
|
||||||
|
PG="psql -h 127.0.0.1 -U ${POSTGRES_USER} -d ${POSTGRES_DB} --no-psqlrc -v ON_ERROR_STOP=0 -q"
|
||||||
|
$PG -f supabase/migrations/000_preflight_supabase_compat.sql || true
|
||||||
|
[ -f supabase/captured_schema.sql ] && $PG -f supabase/captured_schema.sql || true
|
||||||
|
# Concatenate all numbered migrations and run in one session
|
||||||
|
cat supabase/migrations/[0-9]*.sql | $PG
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
@@ -134,17 +137,22 @@ jobs:
|
|||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||||
|
|
||||||
# ── Auth.js v5 (NextAuth) ────────────────────────────────────────
|
# Auth.js v5 (NextAuth). Fall back to Better Auth names if the
|
||||||
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
|
# Gitea secret hasn't been renamed yet.
|
||||||
AUTH_URL: ${{ secrets.AUTH_URL }}
|
AUTH_SECRET: ${{ secrets.AUTH_SECRET || secrets.BETTER_AUTH_SECRET }}
|
||||||
NEXT_PUBLIC_AUTH_URL: ${{ secrets.NEXT_PUBLIC_AUTH_URL }}
|
AUTH_URL: ${{ secrets.AUTH_URL || secrets.BETTER_AUTH_URL }}
|
||||||
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
NEXT_PUBLIC_AUTH_URL: ${{ secrets.NEXT_PUBLIC_AUTH_URL || secrets.NEXT_PUBLIC_BETTER_AUTH_URL }}
|
||||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID || secrets.AUTH_GOOGLE_ID }}
|
||||||
AUTH_GOOGLE_ID: ${{ secrets.AUTH_GOOGLE_ID }}
|
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET || secrets.AUTH_GOOGLE_SECRET }}
|
||||||
AUTH_GOOGLE_SECRET: ${{ secrets.AUTH_GOOGLE_SECRET }}
|
|
||||||
ALLOW_DEV_LOGIN: ${{ secrets.ALLOW_DEV_LOGIN }}
|
ALLOW_DEV_LOGIN: ${{ secrets.ALLOW_DEV_LOGIN }}
|
||||||
|
|
||||||
# ── Storage (MinIO / S3) ─────────────────────────────────────────
|
# Supabase (legacy, still used by admin pages/server actions until
|
||||||
|
# the Auth.js migration is finished)
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
|
||||||
|
|
||||||
|
# Storage (MinIO / S3)
|
||||||
NEXT_PUBLIC_STORAGE_BASE_URL: ${{ secrets.NEXT_PUBLIC_STORAGE_BASE_URL }}
|
NEXT_PUBLIC_STORAGE_BASE_URL: ${{ secrets.NEXT_PUBLIC_STORAGE_BASE_URL }}
|
||||||
STORAGE_ENDPOINT: ${{ secrets.STORAGE_ENDPOINT }}
|
STORAGE_ENDPOINT: ${{ secrets.STORAGE_ENDPOINT }}
|
||||||
STORAGE_REGION: ${{ secrets.STORAGE_REGION }}
|
STORAGE_REGION: ${{ secrets.STORAGE_REGION }}
|
||||||
@@ -152,22 +160,20 @@ jobs:
|
|||||||
STORAGE_SECRET_KEY: ${{ secrets.STORAGE_SECRET_KEY }}
|
STORAGE_SECRET_KEY: ${{ secrets.STORAGE_SECRET_KEY }}
|
||||||
STORAGE_BUCKET_PREFIX: ${{ secrets.STORAGE_BUCKET_PREFIX }}
|
STORAGE_BUCKET_PREFIX: ${{ secrets.STORAGE_BUCKET_PREFIX }}
|
||||||
|
|
||||||
# ── Stripe ───────────────────────────────────────────────────────
|
# Stripe
|
||||||
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
||||||
STRIPE_PUBLISHABLE_KEY: ${{ secrets.STRIPE_PUBLISHABLE_KEY }}
|
STRIPE_PUBLISHABLE_KEY: ${{ secrets.STRIPE_PUBLISHABLE_KEY }}
|
||||||
|
|
||||||
# ── Resend ───────────────────────────────────────────────────────
|
# Resend
|
||||||
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
|
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
|
||||||
RESEND_WEBHOOK_SECRET: ${{ secrets.RESEND_WEBHOOK_SECRET }}
|
RESEND_WEBHOOK_SECRET: ${{ secrets.RESEND_WEBHOOK_SECRET }}
|
||||||
|
|
||||||
# ── AI providers (local code references these) ──────────────────
|
# AI providers
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
MINIMAX_BASE_URL: ${{ secrets.MINIMAX_BASE_URL }}
|
||||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
|
||||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
|
||||||
|
|
||||||
# ── Email sender ─────────────────────────────────────────────────
|
# Email sender
|
||||||
FROM_EMAIL: ${{ secrets.FROM_EMAIL }}
|
FROM_EMAIL: ${{ secrets.FROM_EMAIL }}
|
||||||
run: |
|
run: |
|
||||||
POSTGREST_HOST_PORT=$(cat .postgrest-port)
|
POSTGREST_HOST_PORT=$(cat .postgrest-port)
|
||||||
@@ -184,12 +190,13 @@ jobs:
|
|||||||
MINIO_ROOT_PASSWORD: ${{ secrets.MINIO_ROOT_PASSWORD }}
|
MINIO_ROOT_PASSWORD: ${{ secrets.MINIO_ROOT_PASSWORD }}
|
||||||
POSTGREST_JWT_SECRET: ${{ secrets.POSTGREST_JWT_SECRET }}
|
POSTGREST_JWT_SECRET: ${{ secrets.POSTGREST_JWT_SECRET }}
|
||||||
|
|
||||||
# Auth.js v5
|
# Auth.js v5 (with Better Auth fallback for the secret name)
|
||||||
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
|
AUTH_SECRET: ${{ secrets.AUTH_SECRET || secrets.BETTER_AUTH_SECRET }}
|
||||||
AUTH_URL: ${{ secrets.AUTH_URL }}
|
AUTH_URL: ${{ secrets.AUTH_URL || secrets.BETTER_AUTH_URL }}
|
||||||
NEXT_PUBLIC_AUTH_URL: ${{ secrets.NEXT_PUBLIC_AUTH_URL }}
|
NEXT_PUBLIC_AUTH_URL: ${{ secrets.NEXT_PUBLIC_AUTH_URL || secrets.NEXT_PUBLIC_BETTER_AUTH_URL }}
|
||||||
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID || secrets.AUTH_GOOGLE_ID }}
|
||||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET || secrets.AUTH_GOOGLE_SECRET }}
|
||||||
|
ALLOW_DEV_LOGIN: ${{ secrets.ALLOW_DEV_LOGIN }}
|
||||||
|
|
||||||
# Storage
|
# Storage
|
||||||
STORAGE_ENDPOINT: ${{ secrets.STORAGE_ENDPOINT }}
|
STORAGE_ENDPOINT: ${{ secrets.STORAGE_ENDPOINT }}
|
||||||
@@ -205,6 +212,10 @@ jobs:
|
|||||||
PGRST_DB_ANON_ROLE: ${{ secrets.PGRST_DB_ANON_ROLE }}
|
PGRST_DB_ANON_ROLE: ${{ secrets.PGRST_DB_ANON_ROLE }}
|
||||||
PGRST_JWT_SECRET: ${{ secrets.POSTGREST_JWT_SECRET }}
|
PGRST_JWT_SECRET: ${{ secrets.POSTGREST_JWT_SECRET }}
|
||||||
|
|
||||||
|
# Supabase (legacy)
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
|
||||||
|
|
||||||
# Stripe
|
# Stripe
|
||||||
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
||||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
||||||
@@ -215,10 +226,8 @@ jobs:
|
|||||||
RESEND_WEBHOOK_SECRET: ${{ secrets.RESEND_WEBHOOK_SECRET }}
|
RESEND_WEBHOOK_SECRET: ${{ secrets.RESEND_WEBHOOK_SECRET }}
|
||||||
|
|
||||||
# AI
|
# AI
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
MINIMAX_BASE_URL: ${{ secrets.MINIMAX_BASE_URL }}
|
||||||
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
|
|
||||||
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
|
|
||||||
|
|
||||||
FROM_EMAIL: ${{ secrets.FROM_EMAIL }}
|
FROM_EMAIL: ${{ secrets.FROM_EMAIL }}
|
||||||
run: |
|
run: |
|
||||||
@@ -243,6 +252,7 @@ jobs:
|
|||||||
printf "NEXT_PUBLIC_AUTH_URL=%s\n" "$NEXT_PUBLIC_AUTH_URL"
|
printf "NEXT_PUBLIC_AUTH_URL=%s\n" "$NEXT_PUBLIC_AUTH_URL"
|
||||||
printf "GOOGLE_CLIENT_ID=%s\n" "$GOOGLE_CLIENT_ID"
|
printf "GOOGLE_CLIENT_ID=%s\n" "$GOOGLE_CLIENT_ID"
|
||||||
printf "GOOGLE_CLIENT_SECRET=%s\n" "$GOOGLE_CLIENT_SECRET"
|
printf "GOOGLE_CLIENT_SECRET=%s\n" "$GOOGLE_CLIENT_SECRET"
|
||||||
|
printf "ALLOW_DEV_LOGIN=%s\n" "$ALLOW_DEV_LOGIN"
|
||||||
printf "STORAGE_ENDPOINT=%s\n" "$STORAGE_ENDPOINT"
|
printf "STORAGE_ENDPOINT=%s\n" "$STORAGE_ENDPOINT"
|
||||||
printf "STORAGE_REGION=%s\n" "$STORAGE_REGION"
|
printf "STORAGE_REGION=%s\n" "$STORAGE_REGION"
|
||||||
printf "STORAGE_ACCESS_KEY=%s\n" "$STORAGE_ACCESS_KEY"
|
printf "STORAGE_ACCESS_KEY=%s\n" "$STORAGE_ACCESS_KEY"
|
||||||
@@ -253,15 +263,15 @@ jobs:
|
|||||||
printf "PGRST_DB_URI=%s\n" "$PGRST_DB_URI"
|
printf "PGRST_DB_URI=%s\n" "$PGRST_DB_URI"
|
||||||
printf "PGRST_DB_ANON_ROLE=%s\n" "$PGRST_DB_ANON_ROLE"
|
printf "PGRST_DB_ANON_ROLE=%s\n" "$PGRST_DB_ANON_ROLE"
|
||||||
printf "PGRST_JWT_SECRET=%s\n" "$POSTGREST_JWT_SECRET"
|
printf "PGRST_JWT_SECRET=%s\n" "$POSTGREST_JWT_SECRET"
|
||||||
|
printf "NEXT_PUBLIC_SUPABASE_URL=%s\n" "$NEXT_PUBLIC_SUPABASE_URL"
|
||||||
|
printf "NEXT_PUBLIC_SUPABASE_ANON_KEY=%s\n" "$NEXT_PUBLIC_SUPABASE_ANON_KEY"
|
||||||
printf "STRIPE_SECRET_KEY=%s\n" "$STRIPE_SECRET_KEY"
|
printf "STRIPE_SECRET_KEY=%s\n" "$STRIPE_SECRET_KEY"
|
||||||
printf "STRIPE_WEBHOOK_SECRET=%s\n" "$STRIPE_WEBHOOK_SECRET"
|
printf "STRIPE_WEBHOOK_SECRET=%s\n" "$STRIPE_WEBHOOK_SECRET"
|
||||||
printf "STRIPE_PUBLISHABLE_KEY=%s\n" "$STRIPE_PUBLISHABLE_KEY"
|
printf "STRIPE_PUBLISHABLE_KEY=%s\n" "$STRIPE_PUBLISHABLE_KEY"
|
||||||
printf "RESEND_API_KEY=%s\n" "$RESEND_API_KEY"
|
printf "RESEND_API_KEY=%s\n" "$RESEND_API_KEY"
|
||||||
printf "RESEND_WEBHOOK_SECRET=%s\n" "$RESEND_WEBHOOK_SECRET"
|
printf "RESEND_WEBHOOK_SECRET=%s\n" "$RESEND_WEBHOOK_SECRET"
|
||||||
printf "OPENAI_API_KEY=%s\n" "$OPENAI_API_KEY"
|
printf "MINIMAX_API_KEY=%s\n" "$MINIMAX_API_KEY"
|
||||||
printf "ANTHROPIC_API_KEY=%s\n" "$ANTHROPIC_API_KEY"
|
printf "MINIMAX_BASE_URL=%s\n" "$MINIMAX_BASE_URL"
|
||||||
printf "GOOGLE_API_KEY=%s\n" "$GOOGLE_API_KEY"
|
|
||||||
printf "XAI_API_KEY=%s\n" "$XAI_API_KEY"
|
|
||||||
printf "FROM_EMAIL=%s\n" "$FROM_EMAIL"
|
printf "FROM_EMAIL=%s\n" "$FROM_EMAIL"
|
||||||
} > $APP_DIR/.env.production
|
} > $APP_DIR/.env.production
|
||||||
|
|
||||||
@@ -269,7 +279,7 @@ jobs:
|
|||||||
rsync -a --delete .next/ $APP_DIR/.next/
|
rsync -a --delete .next/ $APP_DIR/.next/
|
||||||
rsync -a --delete public/ $APP_DIR/public/
|
rsync -a --delete public/ $APP_DIR/public/
|
||||||
cp package.json $APP_DIR/
|
cp package.json $APP_DIR/
|
||||||
cp docker-compose.yml $APP_DIR/
|
cp deploy/docker-compose.yml $APP_DIR/
|
||||||
cp -r supabase/ $APP_DIR/
|
cp -r supabase/ $APP_DIR/
|
||||||
cp next.config.ts $APP_DIR/ 2>/dev/null || cp next.config.js $APP_DIR/ 2>/dev/null || true
|
cp next.config.ts $APP_DIR/ 2>/dev/null || cp next.config.js $APP_DIR/ 2>/dev/null || true
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Canonical Remote
|
||||||
|
|
||||||
|
There is exactly one remote — `origin` — pointing to the self-hosted Gitea repo:
|
||||||
|
|
||||||
|
- **URL:** `git@git.crispygoat.com:tyler/route-commerce.git`
|
||||||
|
- **Default branch:** `main`
|
||||||
|
- **Deploy:** push to `origin/main` triggers `.gitea/workflows/deploy.yml`
|
||||||
|
|
||||||
|
Do **not** add GitHub remotes. There is no `origin` on github.com and no separate "dev" repo. If you see `github.com/dzinesco/*` URLs in `.git/config`, that is stale configuration from a previous fork and should be removed (`git remote remove`).
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
Route Commerce is a multi-tenant B2B e-commerce platform for fresh produce wholesale distribution. Brands sell to customers who pick up at scheduled stops or receive shipments. The platform includes admin dashboards for order management, stop/route scheduling, product catalogs, payment processing (Stripe + Square), and a communications module ("Harvest Reach") for email/SMS campaigns.
|
Route Commerce is a multi-tenant B2B e-commerce platform for fresh produce wholesale distribution. Brands sell to customers who pick up at scheduled stops or receive shipments. The platform includes admin dashboards for order management, stop/route scheduling, product catalogs, payment processing (Stripe + Square), and a communications module ("Harvest Reach") for email/SMS campaigns.
|
||||||
|
|||||||
@@ -286,3 +286,25 @@ Follow-up pass on the original Codex review covering public site, buyer path, bi
|
|||||||
### Migration 203 — applied via Supabase CLI
|
### Migration 203 — applied via Supabase CLI
|
||||||
|
|
||||||
`203_plan_usage_active_products.sql` updates `get_brand_plan_info` to count `products` where `active = true AND deleted_at IS NULL`, matching the dashboard's "Active Products" stat. The `NOTIFY pgrst, 'reload schema'` ensures PostgREST picks up the change without restart.
|
`203_plan_usage_active_products.sql` updates `get_brand_plan_info` to count `products` where `active = true AND deleted_at IS NULL`, matching the dashboard's "Active Products" stat. The `NOTIFY pgrst, 'reload schema'` ensures PostgREST picks up the change without restart.
|
||||||
|
|
||||||
|
## Gitea build fix — 2026-06-06
|
||||||
|
|
||||||
|
Gitea runner (`https://git.crispygoat.com/tyler/route-commerce.git`, branch `main`) was failing `next build` with two errors:
|
||||||
|
|
||||||
|
1. **DYNAMIC_SERVER_USAGE** on `/admin/settings/square-sync` (and the whole admin tree): `getAdminUser()` reads `cookies()` via `next/headers`. The admin layout tried to prerender statically, so the first child page that hit cookies aborted the build.
|
||||||
|
2. **Prerender ECONNREFUSED** on `/indian-river-direct/stops`: `getPublicStopsForBrand` / `getActiveStopsForSitemap` / `getBrandSettingsPublic` fetch `NEXT_PUBLIC_SUPABASE_URL` at build time. The Gitea runner passes a Supabase URL that resolves but is unreachable, so `fetch` throws `ECONNREFUSED` and the prerender aborts.
|
||||||
|
|
||||||
|
The earlier commit `2f3be54 fix(actions): skip Supabase fetch at build time when env vars unset` only added `if (!supabaseUrl || !supabaseKey) return [];` — but in CI the env vars **are** set, so the guard passed and the fetch was still attempted.
|
||||||
|
|
||||||
|
### Fixes applied
|
||||||
|
|
||||||
|
- `src/actions/stops.ts` — wrapped `getActiveStopsForSitemap` and `getPublicStopsForBrand` fetches in `try/catch` returning `[]` on error. Env-var guard kept as fast path.
|
||||||
|
- `src/actions/brand-settings.ts` — wrapped `getBrandSettingsPublic` fetch in `try/catch` returning `{ success: false }` on error.
|
||||||
|
- `src/app/admin/settings/square-sync/page.tsx` — added `export const dynamic = "force-dynamic";` (was missing).
|
||||||
|
- `src/app/admin/layout.tsx` — added `export const dynamic = "force-dynamic";` so the entire admin tree opts out of static prerender (layout calls `getAdminUser()` which reads cookies).
|
||||||
|
- `.gitea/workflows/deploy.yml` was simplified earlier in commit `2d837bc` to a thin wrapper around `deploy/deploy.sh`.
|
||||||
|
|
||||||
|
### Remote
|
||||||
|
|
||||||
|
- The crispygoat repo (`git@git.crispygoat.com:tyler/route-commerce.git`) and the GitHub `origin` repo are separate forks — `tyler/main` is the self-hosted Auth.js + Postgres branch, `origin/main` is the Supabase branch. Don't merge them; they share no deploy workflow.
|
||||||
|
- Push targets `tyler/main` to trigger the Gitea build.
|
||||||
|
|||||||
@@ -271,9 +271,18 @@ export async function getBrandSettings(brandId: string): Promise<GetBrandSetting
|
|||||||
|
|
||||||
// Public version for storefront pages — uses slug, no auth required
|
// Public version for storefront pages — uses slug, no auth required
|
||||||
export async function getBrandSettingsPublic(brandSlug: string): Promise<GetBrandSettingsResult & { wholesaleEnabled?: boolean | null }> {
|
export async function getBrandSettingsPublic(brandSlug: string): Promise<GetBrandSettingsResult & { wholesaleEnabled?: boolean | null }> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||||
|
// Build-time prerender runs before Supabase env is configured. Return
|
||||||
|
// a not-configured result; the page falls back to slug-based defaults.
|
||||||
|
if (!supabaseUrl || !supabaseKey) {
|
||||||
|
return { success: false, error: "Supabase not configured", wholesaleEnabled: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapped in try/catch so a build-time Supabase outage (ECONNREFUSED)
|
||||||
|
// doesn't crash the prerender — the page just falls back to its
|
||||||
|
// default brand name and revalidates from a real request later.
|
||||||
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_brand_settings_by_slug`,
|
`${supabaseUrl}/rest/v1/rpc/get_brand_settings_by_slug`,
|
||||||
{
|
{
|
||||||
@@ -290,6 +299,9 @@ export async function getBrandSettingsPublic(brandSlug: string): Promise<GetBran
|
|||||||
settings: data,
|
settings: data,
|
||||||
wholesaleEnabled: data?.wholesale_enabled,
|
wholesaleEnabled: data?.wholesale_enabled,
|
||||||
};
|
};
|
||||||
|
} catch {
|
||||||
|
return { success: false, error: "Failed to fetch brand settings", wholesaleEnabled: undefined };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveBrandSettings(params: {
|
export async function saveBrandSettings(params: {
|
||||||
|
|||||||
+25
-5
@@ -110,10 +110,16 @@ export type StopForSitemap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getActiveStopsForSitemap(): Promise<StopForSitemap[]> {
|
export async function getActiveStopsForSitemap(): Promise<StopForSitemap[]> {
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||||
|
// Build-time prerender runs before Supabase env is configured. Returning
|
||||||
|
// an empty list lets the sitemap render with only the static URLs.
|
||||||
|
if (!supabaseUrl || !supabaseKey) return [];
|
||||||
|
|
||||||
// Get all active stops with their brand slug
|
// Get all active stops with their brand slug. Wrapped in try/catch so a
|
||||||
|
// build-time outage (ECONNREFUSED) doesn't crash the prerender — the
|
||||||
|
// sitemap just renders without stop URLs.
|
||||||
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_active_stops_with_brand`,
|
`${supabaseUrl}/rest/v1/rpc/get_active_stops_with_brand`,
|
||||||
{
|
{
|
||||||
@@ -126,6 +132,9 @@ export async function getActiveStopsForSitemap(): Promise<StopForSitemap[]> {
|
|||||||
|
|
||||||
const stops = await response.json();
|
const stops = await response.json();
|
||||||
return Array.isArray(stops) ? stops : [];
|
return Array.isArray(stops) ? stops : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -155,9 +164,17 @@ export async function getPublicStopsForBrand(
|
|||||||
): Promise<PublicStop[]> {
|
): Promise<PublicStop[]> {
|
||||||
if (!brandSlug) return [];
|
if (!brandSlug) return [];
|
||||||
|
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||||
|
// Build-time prerender runs before Supabase env is configured. Returning
|
||||||
|
// an empty list lets the page render with zero stops; at runtime the
|
||||||
|
// fetch path is unchanged.
|
||||||
|
if (!supabaseUrl || !supabaseKey) return [];
|
||||||
|
|
||||||
|
// Wrapped in try/catch so a build-time Supabase outage (ECONNREFUSED)
|
||||||
|
// doesn't crash the prerender — the page just renders with no stops
|
||||||
|
// and revalidates from a real request once the cache is warm.
|
||||||
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${supabaseUrl}/rest/v1/rpc/get_public_stops_for_brand`,
|
`${supabaseUrl}/rest/v1/rpc/get_public_stops_for_brand`,
|
||||||
{
|
{
|
||||||
@@ -175,4 +192,7 @@ export async function getPublicStopsForBrand(
|
|||||||
|
|
||||||
const stops = await response.json();
|
const stops = await response.json();
|
||||||
return Array.isArray(stops) ? (stops as PublicStop[]) : [];
|
return Array.isArray(stops) ? (stops as PublicStop[]) : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,11 @@ import "@/styles/admin-design-system.css";
|
|||||||
import { ToastProvider } from "@/components/admin/Toast";
|
import { ToastProvider } from "@/components/admin/Toast";
|
||||||
import { ToastContainer } from "@/components/admin/ToastContainer";
|
import { ToastContainer } from "@/components/admin/ToastContainer";
|
||||||
|
|
||||||
|
// Admin layout calls getAdminUser() which reads cookies(). Without this,
|
||||||
|
// Next.js tries to prerender the entire /admin/* tree statically and the
|
||||||
|
// first page that hits cookies() aborts the build with DYNAMIC_SERVER_USAGE.
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
// Toast provider wrapper component
|
// Toast provider wrapper component
|
||||||
function ToastProviderWrapper({ children }: { children: React.ReactNode }) {
|
function ToastProviderWrapper({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { getPaymentSettings } from "@/actions/payments";
|
|||||||
import { getSyncLog, type SyncLogEntry } from "@/actions/square-sync-ui";
|
import { getSyncLog, type SyncLogEntry } from "@/actions/square-sync-ui";
|
||||||
import SquareSyncSettingsClient from "./SquareSyncSettingsClient";
|
import SquareSyncSettingsClient from "./SquareSyncSettingsClient";
|
||||||
|
|
||||||
|
// Uses cookies() via getAdminUser — must be dynamic to avoid the
|
||||||
|
// "couldn't be rendered statically" build error.
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export default async function SquareSyncSettingsPage() {
|
export default async function SquareSyncSettingsPage() {
|
||||||
const adminUser = await getAdminUser();
|
const adminUser = await getAdminUser();
|
||||||
if (!adminUser) redirect("/login");
|
if (!adminUser) redirect("/login");
|
||||||
|
|||||||
Reference in New Issue
Block a user