From 31b0920152405029e35cb9b43b3c33b36d3acfc2 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:06:18 -0700 Subject: [PATCH] feat(ui): enable the synthetic preview session on preview deploys so /app/* screenshots render the real UI (#authed-route-preview) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /app/* dashboard is guarded client-side, so per-PR preview screenshots (reviewbot) captured the sign-in wall instead of the page. useSession().signInPreview already mints a client-only synthetic demo session (login 'local-preview', all roles, no real token) but was gated to `vite dev` only, so it was a no-op on deployed previews. - session.ts: gate signInPreview on PREVIEW_SESSION_ALLOWED (DEV || VITE_PREVIEW==='1'). VITE_PREVIEW is set ONLY by the per-PR preview build; production never sets it, so the escape hatch is dead-code- eliminated from prod builds. - app-shell.tsx: when PREVIEW_SESSION_ALLOWED and the URL has ?preview=1, auto-start the synthetic session once hydration confirms no real session (self-heals via the session dep). Inert in prod / without the param. - ui-preview.yml: set VITE_PREVIEW=1 on the preview Build UI step (the prod ui-deploy.yml build does not). reviewbot appends ?preview=1 to gittensory /app/* routes (shipped separately). Each side is inert without the other. The synthetic session is client-only and grants no real API access — data panels that require a real token still show their unauthenticated/empty state; this renders the authenticated shell + layout for visual review, not live data. --- .github/workflows/ui-preview.yml | 5 +++++ .../src/components/site/app-shell.tsx | 14 ++++++++++++-- apps/gittensory-ui/src/lib/api/session.ts | 13 ++++++++++--- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ui-preview.yml b/.github/workflows/ui-preview.yml index 5ce8082fe5..3cc17c4b92 100644 --- a/.github/workflows/ui-preview.yml +++ b/.github/workflows/ui-preview.yml @@ -48,6 +48,11 @@ jobs: - name: Build UI env: VITE_GITTENSORY_API_ORIGIN: https://gittensory-api.aethereal.dev + # Preview-only: enables the synthetic demo session (useSession().signInPreview) so reviewbot can + # screenshot the authenticated /app/* dashboard via ?preview=1 instead of the sign-in wall. The + # production build (ui-deploy.yml) does NOT set this, so the escape hatch is dead-code-eliminated + # from prod. (#authed-route-preview) + VITE_PREVIEW: "1" run: npm run ui:build # The trusted deploy workflow downloads this by name + run-id. It contains only the built bundle diff --git a/apps/gittensory-ui/src/components/site/app-shell.tsx b/apps/gittensory-ui/src/components/site/app-shell.tsx index 7c91a8113c..b714bd474e 100644 --- a/apps/gittensory-ui/src/components/site/app-shell.tsx +++ b/apps/gittensory-ui/src/components/site/app-shell.tsx @@ -14,7 +14,7 @@ import { import type { ComponentType } from "react"; import { useEffect, useState } from "react"; -import { useSession } from "@/lib/api/session"; +import { PREVIEW_SESSION_ALLOWED, useSession } from "@/lib/api/session"; import { describeApiStatus, pingHealth, useApiStatus } from "@/lib/api/status"; import { Sidebar, @@ -85,7 +85,7 @@ const GROUPS: NavGroup[] = [ ]; export function AppShell() { - const { session, hydrated, signOut } = useSession(); + const { session, hydrated, signOut, signInPreview } = useSession(); const loc = useLocation(); const navigate = useNavigate(); const routerState = useRouterState(); @@ -96,6 +96,16 @@ export function AppShell() { setSidebarOpen(m ? m[1] === "true" : true); }, []); + // Preview deploys: when this is a preview build (VITE_PREVIEW) and the URL carries `?preview=1`, start + // the synthetic demo session automatically once hydration confirms there's no real session. This lets the + // reviewbot screenshot pipeline capture the authenticated /app/* UI instead of the sign-in wall. The + // effect depends on `session`, so it self-heals if a later refresh clears the synthetic session. Inert in + // production (PREVIEW_SESSION_ALLOWED is compiled to false) and without the param. (#authed-route-preview) + useEffect(() => { + if (!PREVIEW_SESSION_ALLOWED || session || !hydrated) return; + if (new URLSearchParams(window.location.search).get("preview") === "1") signInPreview(); + }, [session, hydrated, signInPreview]); + // Keyboard shortcuts: g+o overview, g+w workbench, g+r runs, g+p repos, g+a analytics. useEffect(() => { if (!session) return; diff --git a/apps/gittensory-ui/src/lib/api/session.ts b/apps/gittensory-ui/src/lib/api/session.ts index ed395af0db..85452a0fca 100644 --- a/apps/gittensory-ui/src/lib/api/session.ts +++ b/apps/gittensory-ui/src/lib/api/session.ts @@ -64,6 +64,13 @@ function emitSessionChanged() { if (typeof window !== "undefined") window.dispatchEvent(new Event(SESSION_CHANGED_EVENT)); } +// The synthetic local-preview session is available in `vite dev` (DEV) AND on dedicated preview deploys +// built with VITE_PREVIEW=1 (the per-PR `ui-preview.yml` build sets it; production builds never do, so this +// escape hatch is dead-code-eliminated from prod). It writes no real token and grants only a client-side +// demo session — it lets the reviewbot screenshot pipeline render the authenticated /app/* UI instead of a +// screenshot of the sign-in wall. (#authed-route-preview) +export const PREVIEW_SESSION_ALLOWED = import.meta.env.DEV || import.meta.env.VITE_PREVIEW === "1"; + export function useSession() { const [session, setSession] = useState(null); const [hydrated, setHydrated] = useState(false); @@ -96,7 +103,7 @@ export function useSession() { }; const signInPreview = () => { - if (!import.meta.env.DEV) return; + if (!PREVIEW_SESSION_ALLOWED) return; setSession({ login: "local-preview", roles: ["miner", "maintainer", "owner", "operator"], @@ -104,8 +111,8 @@ export function useSession() { }); setHydrated(true); setAuth({ status: "idle" }); - toast.success("Local preview session started", { - description: "This exists only in dev mode and never writes a production token.", + toast.success("Preview session started", { + description: "A demo session for dev + preview deploys — it never writes a production token.", }); };