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.", }); };