From fb8a677e5af1fb87be059a782cbeb287faffdae1 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 2 Oct 2026 14:35:44 +0000 Subject: [PATCH 1/4] feat(dev): staging-pinned tailnet previews `npm run dev` talks to production unless the browser toggle says staging, so a preview opened from another device could read and write real user data. `npm run dev:preview` binds to 127.0.0.1 and sets VITE_STAGING_PREVIEW=true. The app is then pinned to staging and calls Supabase at /__staging-supabase on its own origin, which Vite proxies to the staging project. Requests stay same-origin, so Edge Function CORS lists need no preview entries and nothing in production changes. The staging strip shows without the switch to production, and the Settings environment switcher is hidden, since a pinned build cannot switch. The project URLs and anon keys move to a side-effect-free projects.ts so vite.config.ts can import them. The proxy path avoids /supabase because Vite serves supabase/functions/_shared there in dev and the frontend imports it. Co-Authored-By: Claude Opus 5.5 (1M context) --- AGENTS.md | 1 + docs/development.md | 12 ++++ docs/operations.md | 2 +- package.json | 1 + src/components/StagingBanner.tsx | 30 +++++----- .../features/settings/EnvironmentSwitcher.tsx | 4 +- src/lib/supabase/env.ts | 56 ++++++++++++++----- src/lib/supabase/projects.ts | 23 ++++++++ src/lib/supabase/switch-env.ts | 5 +- .../components/EnvironmentSwitcher.test.tsx | 23 ++++++++ .../pages/SettingsAdminNavigation.test.tsx | 2 +- src/test/supabase/local-preview-env.test.ts | 52 +++++++++++++++++ src/vite-env.d.ts | 1 + vite.config.ts | 23 ++++++++ 14 files changed, 201 insertions(+), 34 deletions(-) create mode 100644 src/lib/supabase/projects.ts diff --git a/AGENTS.md b/AGENTS.md index 871b4195a..b98cb3570 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,7 @@ The frontend is hosted on Vercel at `https://debate.pi-stack.org`. Merging to `m - Vite development server: `8080` by default outside Superset; Superset allocates `8080-8179` per worktree. - Automated browser server: isolated `127.0.0.1:18080`, never an existing dev server. +- Tailnet previews for Adam: `npm run dev:preview` on `9200-9299`, which pins the app to staging. See [docs/development.md](docs/development.md#preview-on-the-tailnet). - Local Supabase: CLI defaults beginning at `54321`. The stack is not parallel-safe across worktrees. ## Focused verification diff --git a/docs/development.md b/docs/development.md index c809698a3..4cdb9efdc 100644 --- a/docs/development.md +++ b/docs/development.md @@ -49,6 +49,18 @@ npm run dev -- --host 127.0.0.1 --port 8081 Automated browser tests own `127.0.0.1:18080` by default and always start a fresh server. +## Preview on the tailnet + +A plain `npm run dev` talks to the production Supabase project unless the browser toggle says staging. For anything Adam opens from another device, run the staging preview instead: + +```bash +npm run dev:preview -- --port 9201 --strictPort +``` + +`dev:preview` binds to `127.0.0.1` and sets `VITE_STAGING_PREVIEW=true`. The app is then pinned to staging and calls Supabase at `/__staging-supabase` on its own origin, which Vite proxies to the staging project (see `stagingPreviewProxy` in [`vite.config.ts`](../vite.config.ts)). Requests stay same-origin, so Edge Function CORS lists need no preview entries. The staging strip shows, without the switch to production. + +Expose it with the `tailnet-preview` skill on a port in `9200-9299`. Staging Auth allows redirects to `https://codex-remote.tailab0ab3.ts.net:91*/**`, so Google sign-in returns to the preview. Demo codes and email codes must be staging ones: staging has its own users. + ## Environment behavior The checked-in [`.env.example`](../.env.example) contains optional client observability variables: diff --git a/docs/operations.md b/docs/operations.md index 9e8d4f149..e0a34e9bd 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -80,7 +80,7 @@ Staging is project `debate-trainer-staging` (`brifeyypjauyusmkptkx`). Production A migration with a local version and no remote version is pending. A function whose folder changed (`git log -- supabase/functions/ supabase/functions/_shared`) after its remote `updated_at` is stale. Save both lists as the rollback baseline. 3. Apply migrations. Run `supabase db push --project-ref brifeyypjauyusmkptkx --dry-run` and check that it lists exactly the pending files from step 2. Then run it without `--dry-run`. Never use `--include-all` to paper over divergent history. 4. Deploy functions, one at a time, only the ones step 2 marked stale: `supabase functions deploy --project-ref brifeyypjauyusmkptkx --use-api`. [`supabase/config.toml`](../supabase/config.toml) supplies each function's JWT setting. Never use `--prune`. -5. Verify. Rerun step 2; it must show nothing pending. Then start the staging browser suite: dispatch `Protected staging E2E` from `main` with `gh workflow run staging-e2e.yml --ref main`, or, when Actions cannot run, run it locally with the protected values as described in [testing.md](testing.md#protected-staging). For a manual check in the browser, an admin can switch the deployed frontend to staging from Settings and sign in there with a staging demo code. Google and email sign-in on staging work only if the staging Auth redirect URLs include the frontend origin. +5. Verify. Rerun step 2; it must show nothing pending. Then start the staging browser suite: dispatch `Protected staging E2E` from `main` with `gh workflow run staging-e2e.yml --ref main`, or, when Actions cannot run, run it locally with the protected values as described in [testing.md](testing.md#protected-staging). For a manual check in the browser, an admin can switch the deployed frontend to staging from Settings and sign in there with a staging demo code. Google and email sign-in on staging work only if the staging Auth redirect URLs include the frontend origin. Since 2026-10-02 they include tailnet previews (`https://codex-remote.tailab0ab3.ts.net:91*/**`); see [development.md](development.md#preview-on-the-tailnet). Production then follows [Supabase changes](#supabase-changes) with the same list of migrations and functions. diff --git a/package.json b/package.json index 27ba5ba2d..32fe683cf 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "scripts": { "dev": "vite", + "dev:preview": "VITE_STAGING_PREVIEW=true vite --host 127.0.0.1", "docs:check": "node scripts/check-docs.mjs", "backend:status": "node scripts/backend-status.mjs", "build": "vite build && node scripts/check-build-boundaries.mjs", diff --git a/src/components/StagingBanner.tsx b/src/components/StagingBanner.tsx index 30f6ce440..3ad278774 100644 --- a/src/components/StagingBanner.tsx +++ b/src/components/StagingBanner.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { FlaskConical } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useAuth } from '@/lib/supabase/AuthProvider'; -import { getActiveEnv } from '@/lib/supabase/env'; +import { getActiveEnv, isEnvPinned } from '@/lib/supabase/env'; import { reloadPage, switchEnv, useReloadOnEnvChange } from '@/lib/supabase/switch-env'; const BANNER_HEIGHT = '2.25rem'; // h-9 @@ -12,6 +12,8 @@ const BANNER_HEIGHT = '2.25rem'; // h-9 * Shown on every route while staging is active. A fresh staging session starts * signed out and Settings needs sign-in, so this is the way back that always works. * Mounted at the app root in every environment, it also keeps other tabs in step. + * A staging preview build is pinned to staging, so it shows the strip without + * the switch. */ export function StagingBanner({ reload = reloadPage }: { reload?: () => void }) { const { t } = useTranslation(); @@ -37,18 +39,20 @@ export function StagingBanner({ reload = reloadPage }: { reload?: () => void }) >