From d70e201412e98de4a7792d79b323a409870c64b9 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Tue, 15 Jul 2025 18:11:47 -0700 Subject: [PATCH 01/29] impl --- app/components/VersionNotificationBanner.tsx | 49 +++++++++++++ app/root.tsx | 7 +- app/routes/api.version.ts | 76 ++++++++++++++++++++ 3 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 app/components/VersionNotificationBanner.tsx create mode 100644 app/routes/api.version.ts diff --git a/app/components/VersionNotificationBanner.tsx b/app/components/VersionNotificationBanner.tsx new file mode 100644 index 000000000..61fa945a5 --- /dev/null +++ b/app/components/VersionNotificationBanner.tsx @@ -0,0 +1,49 @@ +import { useEffect, useState } from 'react'; +import { toast } from 'sonner'; +import { Button } from '@ui/Button'; +import { SymbolIcon } from '@radix-ui/react-icons'; + +export default function useVersionNotificationBanner() { + const currentSha = process.env.VITE_VERCEL_GIT_COMMIT_SHA; + const [productionSha, setProductionSha] = useState(""); + const [error, setError] = useState(false); + + useEffect(() => { + async function getVersion() { + try { + const res = await fetch("/api/version"); + if (!res.ok) { + throw new Error("Failed to fetch version information"); + } else { + const data = await res.json(); + setProductionSha(data.sha); + } + } catch (e) { + setError(true); + } + } + getVersion(); + }, []); + + if (!error && productionSha && currentSha && productionSha !== currentSha) { + toast.info( +
+ A new version of Chef is available! Refresh this page to update. + +
, + { + id: "dashboardVersion", + duration: Number.POSITIVE_INFINITY, + } + ); + } +} diff --git a/app/root.tsx b/app/root.tsx index 822806d3a..0437bb588 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -21,6 +21,7 @@ import posthog from 'posthog-js'; import 'allotment/dist/style.css'; import { ErrorDisplay } from './components/ErrorComponent'; +import useVersionNotificationBanner from './components/VersionNotificationBanner'; export async function loader() { // These environment variables are available in the client (they aren't secret). @@ -130,6 +131,8 @@ export function Layout({ children }: { children: React.ReactNode }) { }); }, []); + useVersionNotificationBanner(); + return ( <> @@ -144,7 +147,9 @@ export function Layout({ children }: { children: React.ReactNode }) { useRefreshTokens={true} cacheLocation="localstorage" > - {children} + + {children} + )} diff --git a/app/routes/api.version.ts b/app/routes/api.version.ts new file mode 100644 index 000000000..3756a7c8d --- /dev/null +++ b/app/routes/api.version.ts @@ -0,0 +1,76 @@ +import { json } from '@vercel/remix'; +import type { LoaderFunctionArgs } from '@vercel/remix'; + +export async function loader({ request: _request }: LoaderFunctionArgs) { + const projectId = process.env.VERCEL_PROJECT_ID; + const teamId = process.env.VERCEL_TEAM_ID; + const productionBranchUrl = process.env.VERCEL_PRODUCTION_BRANCH_URL || 'https://chef.convex.dev'; + + if (!process.env.VERCEL_TOKEN) { + return json({ error: 'Failed to fetch version information' }, { status: 500 }); + } + + const requestOptions = { + headers: { + Authorization: `Bearer ${process.env.VERCEL_TOKEN}`, + }, + method: 'get', + }; + + console.log('process.env.VERCEL_ENV', process.env.VERCEL_ENV, process.env.VERCEL_ENV !== 'preview'); + + if (process.env.VERCEL_ENV !== 'preview') { + // If we're not in a preview deployment, fetch the production deployment from Vercel's undocumented + // production-deployment API. + // This response includes a boolean indicating if the production deployment is stale (i.e. rolled back) + // We accept the risk that this API might change because it is not documented, meaning the version + // notification feature might silently fail. + const prodResponse = await fetch( + `https://vercel.com/api/v1/projects/${projectId}/production-deployment?teamId=${teamId}`, + requestOptions, + ); + console.log(prodResponse); + if (!prodResponse.ok) { + return json({ error: 'Failed to fetch production version information' }, { status: 500 }); + } + + const prodData = await prodResponse.json(); + + // Since we retrieved data from an undocumented API + // let's defensively check that the data we need is present + // and return an opaque error if it isn't. + if (!prodData || typeof prodData.deploymentIsStale !== 'boolean') { + return json({ error: 'Failed to fetch production deployment' }, { status: 500 }); + } + + // If the production deployment is rolled back, + // we should not show a version notification. + if (prodData.deploymentIsStale) { + console.log('production deployment is stale'); + return json({ sha: null }, { status: 200 }); + } + } + + // Even though we retrieved the production data, we might be on a preview branch deployment. + // So, fetch the data specific to the latest branch deployment. + const branchUrl = process.env.VERCEL_BRANCH_URL || productionBranchUrl; + if (!branchUrl) { + throw new Error('VERCEL_BRANCH_URL or VERCEL_PRODUCTION_BRANCH_URL not set'); + } + const branchResponse = await fetch( + `https://api.vercel.com/v13/deployments/${branchUrl}?teamId=${teamId}`, + requestOptions, + ); + if (!branchResponse.ok) { + throw new Error('Failed to fetch branch version information'); + } + + const branchData = await branchResponse.json(); + + return json( + { + sha: branchData.gitSource.sha, + }, + { status: 200 }, + ); +} From a837e6dadd2a161f53b1891c665373ce2bf3acfc Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Tue, 15 Jul 2025 18:14:39 -0700 Subject: [PATCH 02/29] nit --- app/components/VersionNotificationBanner.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/VersionNotificationBanner.tsx b/app/components/VersionNotificationBanner.tsx index 61fa945a5..f4461e0ad 100644 --- a/app/components/VersionNotificationBanner.tsx +++ b/app/components/VersionNotificationBanner.tsx @@ -41,7 +41,7 @@ export default function useVersionNotificationBanner() { , { - id: "dashboardVersion", + id: "chefVersion", duration: Number.POSITIVE_INFINITY, } ); From 19e14a59ce5e552ce885128ae33d8fca335cb69c Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Tue, 15 Jul 2025 18:15:50 -0700 Subject: [PATCH 03/29] formatting --- app/components/VersionNotificationBanner.tsx | 12 ++++++------ app/root.tsx | 4 +--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/components/VersionNotificationBanner.tsx b/app/components/VersionNotificationBanner.tsx index f4461e0ad..9d4f2d7ab 100644 --- a/app/components/VersionNotificationBanner.tsx +++ b/app/components/VersionNotificationBanner.tsx @@ -5,20 +5,20 @@ import { SymbolIcon } from '@radix-ui/react-icons'; export default function useVersionNotificationBanner() { const currentSha = process.env.VITE_VERCEL_GIT_COMMIT_SHA; - const [productionSha, setProductionSha] = useState(""); + const [productionSha, setProductionSha] = useState(''); const [error, setError] = useState(false); useEffect(() => { async function getVersion() { try { - const res = await fetch("/api/version"); + const res = await fetch('/api/version'); if (!res.ok) { - throw new Error("Failed to fetch version information"); + throw new Error('Failed to fetch version information'); } else { const data = await res.json(); setProductionSha(data.sha); } - } catch (e) { + } catch (_e) { setError(true); } } @@ -41,9 +41,9 @@ export default function useVersionNotificationBanner() { , { - id: "chefVersion", + id: 'chefVersion', duration: Number.POSITIVE_INFINITY, - } + }, ); } } diff --git a/app/root.tsx b/app/root.tsx index 0437bb588..c43d0e829 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -147,9 +147,7 @@ export function Layout({ children }: { children: React.ReactNode }) { useRefreshTokens={true} cacheLocation="localstorage" > - - {children} - + {children} )} From 1c9ff4b186f8f0fd7169690586d55ce26a7dea18 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Wed, 16 Jul 2025 17:14:40 -0700 Subject: [PATCH 04/29] testing --- app/components/VersionNotificationBanner.tsx | 8 ++++++++ app/routes/api.version.ts | 6 +----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/components/VersionNotificationBanner.tsx b/app/components/VersionNotificationBanner.tsx index 9d4f2d7ab..8a585487d 100644 --- a/app/components/VersionNotificationBanner.tsx +++ b/app/components/VersionNotificationBanner.tsx @@ -7,6 +7,9 @@ export default function useVersionNotificationBanner() { const currentSha = process.env.VITE_VERCEL_GIT_COMMIT_SHA; const [productionSha, setProductionSha] = useState(''); const [error, setError] = useState(false); + console.log('currentSha', currentSha); + console.log('VITE_VERCEL_GIT_COMMIT_SHA', process.env.VITE_VERCEL_GIT_COMMIT_SHA); + console.log('VERCEL_GIT_COMMIT_SHA', process.env.VERCEL_GIT_COMMIT_SHA); useEffect(() => { async function getVersion() { @@ -23,6 +26,11 @@ export default function useVersionNotificationBanner() { } } getVersion(); + + const interval = setInterval(getVersion, 60 * 60 * 1000); + + // Cleanup interval on unmount + return () => clearInterval(interval); }, []); if (!error && productionSha && currentSha && productionSha !== currentSha) { diff --git a/app/routes/api.version.ts b/app/routes/api.version.ts index 3756a7c8d..7b91526de 100644 --- a/app/routes/api.version.ts +++ b/app/routes/api.version.ts @@ -4,7 +4,7 @@ import type { LoaderFunctionArgs } from '@vercel/remix'; export async function loader({ request: _request }: LoaderFunctionArgs) { const projectId = process.env.VERCEL_PROJECT_ID; const teamId = process.env.VERCEL_TEAM_ID; - const productionBranchUrl = process.env.VERCEL_PRODUCTION_BRANCH_URL || 'https://chef.convex.dev'; + const productionBranchUrl = process.env.VERCEL_PRODUCTION_BRANCH_URL || 'chef.convex.dev'; if (!process.env.VERCEL_TOKEN) { return json({ error: 'Failed to fetch version information' }, { status: 500 }); @@ -17,8 +17,6 @@ export async function loader({ request: _request }: LoaderFunctionArgs) { method: 'get', }; - console.log('process.env.VERCEL_ENV', process.env.VERCEL_ENV, process.env.VERCEL_ENV !== 'preview'); - if (process.env.VERCEL_ENV !== 'preview') { // If we're not in a preview deployment, fetch the production deployment from Vercel's undocumented // production-deployment API. @@ -29,7 +27,6 @@ export async function loader({ request: _request }: LoaderFunctionArgs) { `https://vercel.com/api/v1/projects/${projectId}/production-deployment?teamId=${teamId}`, requestOptions, ); - console.log(prodResponse); if (!prodResponse.ok) { return json({ error: 'Failed to fetch production version information' }, { status: 500 }); } @@ -46,7 +43,6 @@ export async function loader({ request: _request }: LoaderFunctionArgs) { // If the production deployment is rolled back, // we should not show a version notification. if (prodData.deploymentIsStale) { - console.log('production deployment is stale'); return json({ sha: null }, { status: 200 }); } } From 1e9bfe1fbc817dc111ec9fd88ea378f10655db68 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Wed, 16 Jul 2025 17:33:34 -0700 Subject: [PATCH 05/29] update vite config --- app/components/VersionNotificationBanner.tsx | 5 +---- vite.config.ts | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/components/VersionNotificationBanner.tsx b/app/components/VersionNotificationBanner.tsx index 8a585487d..a3974a4bc 100644 --- a/app/components/VersionNotificationBanner.tsx +++ b/app/components/VersionNotificationBanner.tsx @@ -4,12 +4,9 @@ import { Button } from '@ui/Button'; import { SymbolIcon } from '@radix-ui/react-icons'; export default function useVersionNotificationBanner() { - const currentSha = process.env.VITE_VERCEL_GIT_COMMIT_SHA; + const currentSha = import.meta.env.VERCEL_GIT_COMMIT_SHA; const [productionSha, setProductionSha] = useState(''); const [error, setError] = useState(false); - console.log('currentSha', currentSha); - console.log('VITE_VERCEL_GIT_COMMIT_SHA', process.env.VITE_VERCEL_GIT_COMMIT_SHA); - console.log('VERCEL_GIT_COMMIT_SHA', process.env.VERCEL_GIT_COMMIT_SHA); useEffect(() => { async function getVersion() { diff --git a/vite.config.ts b/vite.config.ts index 775c3bc18..9f6babd3b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -14,6 +14,7 @@ export default defineConfig((config) => { return { define: { 'process.env.VERCEL_ENV': JSON.stringify(process.env.VERCEL_ENV), + 'process.env.VERCEL_GIT_COMMIT_SHA': JSON.stringify(process.env.VERCEL_GIT_COMMIT_SHA), }, /* From ae1915f8a35ceca7c0ea31f003676381c2189753 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Wed, 16 Jul 2025 17:37:09 -0700 Subject: [PATCH 06/29] nit --- app/components/VersionNotificationBanner.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/components/VersionNotificationBanner.tsx b/app/components/VersionNotificationBanner.tsx index a3974a4bc..b80a5b51b 100644 --- a/app/components/VersionNotificationBanner.tsx +++ b/app/components/VersionNotificationBanner.tsx @@ -4,10 +4,12 @@ import { Button } from '@ui/Button'; import { SymbolIcon } from '@radix-ui/react-icons'; export default function useVersionNotificationBanner() { - const currentSha = import.meta.env.VERCEL_GIT_COMMIT_SHA; + const currentSha = process.env.VERCEL_GIT_COMMIT_SHA; const [productionSha, setProductionSha] = useState(''); const [error, setError] = useState(false); + console.log('currentSha', currentSha); + useEffect(() => { async function getVersion() { try { From b92507e7e6fed08fbbddf049b4c25d7b2a4ac505 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Wed, 16 Jul 2025 17:37:59 -0700 Subject: [PATCH 07/29] test --- app/components/VersionNotificationBanner.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/components/VersionNotificationBanner.tsx b/app/components/VersionNotificationBanner.tsx index b80a5b51b..04ba785c1 100644 --- a/app/components/VersionNotificationBanner.tsx +++ b/app/components/VersionNotificationBanner.tsx @@ -19,6 +19,7 @@ export default function useVersionNotificationBanner() { } else { const data = await res.json(); setProductionSha(data.sha); + console.log('productionSha', productionSha); } } catch (_e) { setError(true); @@ -26,7 +27,8 @@ export default function useVersionNotificationBanner() { } getVersion(); - const interval = setInterval(getVersion, 60 * 60 * 1000); + // TODO: Change to 60 * 60 * 1000 + const interval = setInterval(getVersion, 1000); // Cleanup interval on unmount return () => clearInterval(interval); From 2bc3ad768cb41b114f0384864081d6ed75f8ad04 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Wed, 16 Jul 2025 17:44:40 -0700 Subject: [PATCH 08/29] more testing --- app/components/VersionNotificationBanner.tsx | 1 + app/routes/api.version.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/app/components/VersionNotificationBanner.tsx b/app/components/VersionNotificationBanner.tsx index 04ba785c1..df5416d91 100644 --- a/app/components/VersionNotificationBanner.tsx +++ b/app/components/VersionNotificationBanner.tsx @@ -14,6 +14,7 @@ export default function useVersionNotificationBanner() { async function getVersion() { try { const res = await fetch('/api/version'); + console.log('res', res); if (!res.ok) { throw new Error('Failed to fetch version information'); } else { diff --git a/app/routes/api.version.ts b/app/routes/api.version.ts index 7b91526de..b7ff5f653 100644 --- a/app/routes/api.version.ts +++ b/app/routes/api.version.ts @@ -50,7 +50,9 @@ export async function loader({ request: _request }: LoaderFunctionArgs) { // Even though we retrieved the production data, we might be on a preview branch deployment. // So, fetch the data specific to the latest branch deployment. const branchUrl = process.env.VERCEL_BRANCH_URL || productionBranchUrl; + console.log('branchUrl', branchUrl); if (!branchUrl) { + console.log('branchUrl not set'); throw new Error('VERCEL_BRANCH_URL or VERCEL_PRODUCTION_BRANCH_URL not set'); } const branchResponse = await fetch( @@ -58,10 +60,12 @@ export async function loader({ request: _request }: LoaderFunctionArgs) { requestOptions, ); if (!branchResponse.ok) { + console.log('branchResponse not ok'); throw new Error('Failed to fetch branch version information'); } const branchData = await branchResponse.json(); + console.log('branchData', branchData); return json( { From c5b36344c6aa4be92b6a38416ed008b2faf1aa02 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Thu, 17 Jul 2025 14:11:27 -0700 Subject: [PATCH 09/29] testing --- app/routes/api.version.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/routes/api.version.ts b/app/routes/api.version.ts index b7ff5f653..d2efd5a12 100644 --- a/app/routes/api.version.ts +++ b/app/routes/api.version.ts @@ -6,6 +6,8 @@ export async function loader({ request: _request }: LoaderFunctionArgs) { const teamId = process.env.VERCEL_TEAM_ID; const productionBranchUrl = process.env.VERCEL_PRODUCTION_BRANCH_URL || 'chef.convex.dev'; + console.log('vercel token', process.env.VERCEL_TOKEN); + if (!process.env.VERCEL_TOKEN) { return json({ error: 'Failed to fetch version information' }, { status: 500 }); } From 81b7e0e18c733f1687b09391adcc1d8c9b7aa09a Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Thu, 17 Jul 2025 14:19:59 -0700 Subject: [PATCH 10/29] testing --- app/routes/api.version.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/routes/api.version.ts b/app/routes/api.version.ts index d2efd5a12..7ea2831ab 100644 --- a/app/routes/api.version.ts +++ b/app/routes/api.version.ts @@ -6,6 +6,9 @@ export async function loader({ request: _request }: LoaderFunctionArgs) { const teamId = process.env.VERCEL_TEAM_ID; const productionBranchUrl = process.env.VERCEL_PRODUCTION_BRANCH_URL || 'chef.convex.dev'; + console.log('vercel project id', projectId); + console.log('vercel team id', teamId); + console.log('vercel production branch url', productionBranchUrl); console.log('vercel token', process.env.VERCEL_TOKEN); if (!process.env.VERCEL_TOKEN) { From 0153ddd9d4310401e41804a85bfd215e8fd1fe5f Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Thu, 17 Jul 2025 14:22:37 -0700 Subject: [PATCH 11/29] log env --- app/routes/api.version.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/routes/api.version.ts b/app/routes/api.version.ts index 7ea2831ab..549383927 100644 --- a/app/routes/api.version.ts +++ b/app/routes/api.version.ts @@ -11,6 +11,8 @@ export async function loader({ request: _request }: LoaderFunctionArgs) { console.log('vercel production branch url', productionBranchUrl); console.log('vercel token', process.env.VERCEL_TOKEN); + console.log('process.env', process.env); + if (!process.env.VERCEL_TOKEN) { return json({ error: 'Failed to fetch version information' }, { status: 500 }); } From a8a03ae43e2641888ae903eebe894ad5e48e62a5 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Thu, 17 Jul 2025 14:33:05 -0700 Subject: [PATCH 12/29] testing --- app/components/VersionNotificationBanner.tsx | 4 +++- app/routes/api.enhance-prompt.ts | 2 ++ app/routes/api.version.ts | 10 ++++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/components/VersionNotificationBanner.tsx b/app/components/VersionNotificationBanner.tsx index df5416d91..378cbd193 100644 --- a/app/components/VersionNotificationBanner.tsx +++ b/app/components/VersionNotificationBanner.tsx @@ -13,7 +13,9 @@ export default function useVersionNotificationBanner() { useEffect(() => { async function getVersion() { try { - const res = await fetch('/api/version'); + const res = await fetch('/api/version', { + method: 'POST', + }); console.log('res', res); if (!res.ok) { throw new Error('Failed to fetch version information'); diff --git a/app/routes/api.enhance-prompt.ts b/app/routes/api.enhance-prompt.ts index 57a0365c0..1448800b2 100644 --- a/app/routes/api.enhance-prompt.ts +++ b/app/routes/api.enhance-prompt.ts @@ -117,6 +117,8 @@ export async function action({ request }: ActionFunctionArgs) { }); } + console.log('process.env.OPENAI_API_KEY', process.env.OPENAI_API_KEY?.length); + const { prompt } = await request.json(); if (!prompt || typeof prompt !== 'string') { diff --git a/app/routes/api.version.ts b/app/routes/api.version.ts index 549383927..291f55284 100644 --- a/app/routes/api.version.ts +++ b/app/routes/api.version.ts @@ -1,7 +1,13 @@ import { json } from '@vercel/remix'; -import type { LoaderFunctionArgs } from '@vercel/remix'; +import type { ActionFunctionArgs } from '@vercel/remix'; -export async function loader({ request: _request }: LoaderFunctionArgs) { +export async function action({ request }: ActionFunctionArgs) { + if (request.method !== 'POST') { + return new Response(JSON.stringify({ error: 'Method not allowed' }), { + status: 405, + headers: { 'Content-Type': 'application/json' }, + }); + } const projectId = process.env.VERCEL_PROJECT_ID; const teamId = process.env.VERCEL_TEAM_ID; const productionBranchUrl = process.env.VERCEL_PRODUCTION_BRANCH_URL || 'chef.convex.dev'; From 788dc5aa37da212bbb2f65b9bfac80d4f8d1f37e Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Thu, 17 Jul 2025 14:55:27 -0700 Subject: [PATCH 13/29] more testing --- app/routes/api.version.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/routes/api.version.ts b/app/routes/api.version.ts index 291f55284..c28374b78 100644 --- a/app/routes/api.version.ts +++ b/app/routes/api.version.ts @@ -17,6 +17,11 @@ export async function action({ request }: ActionFunctionArgs) { console.log('vercel production branch url', productionBranchUrl); console.log('vercel token', process.env.VERCEL_TOKEN); + console.log('process.env?.[VERCEL_TOKEN]?.trim()', process.env.VERCEL_TOKEN?.trim()); + console.log('process.env?.[VERCEL_TEAM_ID]?.trim()', process.env.VERCEL_TEAM_ID?.trim()); + console.log('process.env?.[VERCEL_PROJECT_ID]?.trim()', process.env.VERCEL_PROJECT_ID?.trim()); + console.log('process.env?.[VERCEL_PRODUCTION_BRANCH_URL]?.trim()', process.env.VERCEL_PRODUCTION_BRANCH_URL?.trim()); + console.log('process.env', process.env); if (!process.env.VERCEL_TOKEN) { From 43774245825ef58ac9e615535794ef562e707f74 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Thu, 17 Jul 2025 15:04:42 -0700 Subject: [PATCH 14/29] testing --- app/routes/api.enhance-prompt.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/routes/api.enhance-prompt.ts b/app/routes/api.enhance-prompt.ts index 1448800b2..e72efe6b8 100644 --- a/app/routes/api.enhance-prompt.ts +++ b/app/routes/api.enhance-prompt.ts @@ -132,6 +132,8 @@ export async function action({ request }: ActionFunctionArgs) { apiKey: process.env.OPENAI_API_KEY, }); + console.log('openai.apiKey', openai.apiKey); + const completion = await openai.chat.completions.create({ model: 'gpt-4.1-mini', messages: [ From 66105bee28d675c4f614890cbefd4163d58f7d36 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Thu, 17 Jul 2025 16:08:31 -0700 Subject: [PATCH 15/29] more testing --- app/routes/api.version.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/routes/api.version.ts b/app/routes/api.version.ts index c28374b78..6e637b87e 100644 --- a/app/routes/api.version.ts +++ b/app/routes/api.version.ts @@ -1,6 +1,8 @@ import { json } from '@vercel/remix'; import type { ActionFunctionArgs } from '@vercel/remix'; +declare const Deno: any; + export async function action({ request }: ActionFunctionArgs) { if (request.method !== 'POST') { return new Response(JSON.stringify({ error: 'Method not allowed' }), { @@ -24,6 +26,10 @@ export async function action({ request }: ActionFunctionArgs) { console.log('process.env', process.env); + console.log('Deno', Deno); + console.log('Deno.env', Deno.env); + console.log('Deno.env.get("VERCEL_TOKEN")', Deno.env.get('VERCEL_TOKEN')); + if (!process.env.VERCEL_TOKEN) { return json({ error: 'Failed to fetch version information' }, { status: 500 }); } From fb9aebde9a8b15713212def232659c68c08d1602 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Thu, 17 Jul 2025 16:18:02 -0700 Subject: [PATCH 16/29] even more testing --- app/routes/api.version.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/app/routes/api.version.ts b/app/routes/api.version.ts index 6e637b87e..91e25ad60 100644 --- a/app/routes/api.version.ts +++ b/app/routes/api.version.ts @@ -3,13 +3,7 @@ import type { ActionFunctionArgs } from '@vercel/remix'; declare const Deno: any; -export async function action({ request }: ActionFunctionArgs) { - if (request.method !== 'POST') { - return new Response(JSON.stringify({ error: 'Method not allowed' }), { - status: 405, - headers: { 'Content-Type': 'application/json' }, - }); - } +async function fetchVersionInfo() { const projectId = process.env.VERCEL_PROJECT_ID; const teamId = process.env.VERCEL_TEAM_ID; const productionBranchUrl = process.env.VERCEL_PRODUCTION_BRANCH_URL || 'chef.convex.dev'; @@ -26,10 +20,6 @@ export async function action({ request }: ActionFunctionArgs) { console.log('process.env', process.env); - console.log('Deno', Deno); - console.log('Deno.env', Deno.env); - console.log('Deno.env.get("VERCEL_TOKEN")', Deno.env.get('VERCEL_TOKEN')); - if (!process.env.VERCEL_TOKEN) { return json({ error: 'Failed to fetch version information' }, { status: 500 }); } @@ -98,3 +88,14 @@ export async function action({ request }: ActionFunctionArgs) { { status: 200 }, ); } + +export async function action({ request }: ActionFunctionArgs) { + if (request.method !== 'POST') { + return new Response(JSON.stringify({ error: 'Method not allowed' }), { + status: 405, + headers: { 'Content-Type': 'application/json' }, + }); + } + + return await fetchVersionInfo(); +} From 1377f591b05cdf2039bfb5c32aeb3a40484c7f91 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Thu, 17 Jul 2025 16:37:12 -0700 Subject: [PATCH 17/29] more testing --- app/routes/api.version.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/routes/api.version.ts b/app/routes/api.version.ts index 91e25ad60..353282e83 100644 --- a/app/routes/api.version.ts +++ b/app/routes/api.version.ts @@ -4,34 +4,34 @@ import type { ActionFunctionArgs } from '@vercel/remix'; declare const Deno: any; async function fetchVersionInfo() { - const projectId = process.env.VERCEL_PROJECT_ID; - const teamId = process.env.VERCEL_TEAM_ID; - const productionBranchUrl = process.env.VERCEL_PRODUCTION_BRANCH_URL || 'chef.convex.dev'; + const projectId = globalThis.process.env.VERCEL_PROJECT_ID; + const teamId = globalThis.process.env.VERCEL_TEAM_ID; + const productionBranchUrl = globalThis.process.env.VERCEL_PRODUCTION_BRANCH_URL || 'chef.convex.dev'; console.log('vercel project id', projectId); console.log('vercel team id', teamId); console.log('vercel production branch url', productionBranchUrl); - console.log('vercel token', process.env.VERCEL_TOKEN); + console.log('vercel token', globalThis.process.env.VERCEL_TOKEN); - console.log('process.env?.[VERCEL_TOKEN]?.trim()', process.env.VERCEL_TOKEN?.trim()); - console.log('process.env?.[VERCEL_TEAM_ID]?.trim()', process.env.VERCEL_TEAM_ID?.trim()); - console.log('process.env?.[VERCEL_PROJECT_ID]?.trim()', process.env.VERCEL_PROJECT_ID?.trim()); - console.log('process.env?.[VERCEL_PRODUCTION_BRANCH_URL]?.trim()', process.env.VERCEL_PRODUCTION_BRANCH_URL?.trim()); + console.log('globalThis.process.env?.[VERCEL_TOKEN]?.trim()', globalThis.process.env.VERCEL_TOKEN?.trim()); + console.log('globalThis.process.env?.[VERCEL_TEAM_ID]?.trim()', globalThis.process.env.VERCEL_TEAM_ID?.trim()); + console.log('globalThis.process.env?.[VERCEL_PROJECT_ID]?.trim()', globalThis.process.env.VERCEL_PROJECT_ID?.trim()); + console.log('globalThis.process.env?.[VERCEL_PRODUCTION_BRANCH_URL]?.trim()', globalThis.process.env.VERCEL_PRODUCTION_BRANCH_URL?.trim()); - console.log('process.env', process.env); + console.log('globalThis.process.env', globalThis.process.env); - if (!process.env.VERCEL_TOKEN) { + if (!globalThis.process.env.VERCEL_TOKEN) { return json({ error: 'Failed to fetch version information' }, { status: 500 }); } const requestOptions = { headers: { - Authorization: `Bearer ${process.env.VERCEL_TOKEN}`, + Authorization: `Bearer ${globalThis.process.env.VERCEL_TOKEN}`, }, method: 'get', }; - if (process.env.VERCEL_ENV !== 'preview') { + if (globalThis.process.env.VERCEL_ENV !== 'preview') { // If we're not in a preview deployment, fetch the production deployment from Vercel's undocumented // production-deployment API. // This response includes a boolean indicating if the production deployment is stale (i.e. rolled back) @@ -63,7 +63,7 @@ async function fetchVersionInfo() { // Even though we retrieved the production data, we might be on a preview branch deployment. // So, fetch the data specific to the latest branch deployment. - const branchUrl = process.env.VERCEL_BRANCH_URL || productionBranchUrl; + const branchUrl = globalThis.process.env.VERCEL_BRANCH_URL || productionBranchUrl; console.log('branchUrl', branchUrl); if (!branchUrl) { console.log('branchUrl not set'); From 0221559af4836979c4004cc49b89e9fbfe6f4043 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Thu, 17 Jul 2025 17:26:36 -0700 Subject: [PATCH 18/29] impl eslint rule + use globalThis --- app/components/VersionNotificationBanner.tsx | 3 +- app/entry.client.tsx | 1 + app/root.tsx | 1 + app/routes/api.enhance-prompt.ts | 4 +- app/routes/api.version.ts | 43 +++-------- eslint.config.mjs | 35 +++++++++ template/eslint.config.js | 77 -------------------- 7 files changed, 52 insertions(+), 112 deletions(-) delete mode 100644 template/eslint.config.js diff --git a/app/components/VersionNotificationBanner.tsx b/app/components/VersionNotificationBanner.tsx index 378cbd193..460487ea1 100644 --- a/app/components/VersionNotificationBanner.tsx +++ b/app/components/VersionNotificationBanner.tsx @@ -4,6 +4,7 @@ import { Button } from '@ui/Button'; import { SymbolIcon } from '@radix-ui/react-icons'; export default function useVersionNotificationBanner() { + // @eslint-disable-next-line local/no-direct-process-env const currentSha = process.env.VERCEL_GIT_COMMIT_SHA; const [productionSha, setProductionSha] = useState(''); const [error, setError] = useState(false); @@ -35,7 +36,7 @@ export default function useVersionNotificationBanner() { // Cleanup interval on unmount return () => clearInterval(interval); - }, []); + }, [productionSha]); if (!error && productionSha && currentSha && productionSha !== currentSha) { toast.info( diff --git a/app/entry.client.tsx b/app/entry.client.tsx index bdffc1c6b..befbd4153 100644 --- a/app/entry.client.tsx +++ b/app/entry.client.tsx @@ -3,6 +3,7 @@ import { RemixBrowser, useLocation, useMatches } from '@remix-run/react'; import { startTransition, useEffect } from 'react'; import { hydrateRoot } from 'react-dom/client'; +// @eslint-disable-next-line local/no-direct-process-env const environment = process.env.VERCEL_ENV === 'production' ? 'production' : 'development'; Sentry.init({ diff --git a/app/root.tsx b/app/root.tsx index c43d0e829..0d9a555d5 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -25,6 +25,7 @@ import useVersionNotificationBanner from './components/VersionNotificationBanner export async function loader() { // These environment variables are available in the client (they aren't secret). + // @eslint-disable-next-line local/no-direct-process-env const CONVEX_URL = process.env.VITE_CONVEX_URL || globalThis.process.env.CONVEX_URL!; const CONVEX_OAUTH_CLIENT_ID = globalThis.process.env.CONVEX_OAUTH_CLIENT_ID!; return json({ diff --git a/app/routes/api.enhance-prompt.ts b/app/routes/api.enhance-prompt.ts index e72efe6b8..60acb220c 100644 --- a/app/routes/api.enhance-prompt.ts +++ b/app/routes/api.enhance-prompt.ts @@ -117,8 +117,6 @@ export async function action({ request }: ActionFunctionArgs) { }); } - console.log('process.env.OPENAI_API_KEY', process.env.OPENAI_API_KEY?.length); - const { prompt } = await request.json(); if (!prompt || typeof prompt !== 'string') { @@ -129,7 +127,7 @@ export async function action({ request }: ActionFunctionArgs) { } const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, + apiKey: globalThis.process.env.OPENAI_API_KEY, }); console.log('openai.apiKey', openai.apiKey); diff --git a/app/routes/api.version.ts b/app/routes/api.version.ts index 353282e83..9238c8846 100644 --- a/app/routes/api.version.ts +++ b/app/routes/api.version.ts @@ -1,26 +1,20 @@ import { json } from '@vercel/remix'; import type { ActionFunctionArgs } from '@vercel/remix'; -declare const Deno: any; - -async function fetchVersionInfo() { - const projectId = globalThis.process.env.VERCEL_PROJECT_ID; - const teamId = globalThis.process.env.VERCEL_TEAM_ID; - const productionBranchUrl = globalThis.process.env.VERCEL_PRODUCTION_BRANCH_URL || 'chef.convex.dev'; - - console.log('vercel project id', projectId); - console.log('vercel team id', teamId); - console.log('vercel production branch url', productionBranchUrl); - console.log('vercel token', globalThis.process.env.VERCEL_TOKEN); - - console.log('globalThis.process.env?.[VERCEL_TOKEN]?.trim()', globalThis.process.env.VERCEL_TOKEN?.trim()); - console.log('globalThis.process.env?.[VERCEL_TEAM_ID]?.trim()', globalThis.process.env.VERCEL_TEAM_ID?.trim()); - console.log('globalThis.process.env?.[VERCEL_PROJECT_ID]?.trim()', globalThis.process.env.VERCEL_PROJECT_ID?.trim()); - console.log('globalThis.process.env?.[VERCEL_PRODUCTION_BRANCH_URL]?.trim()', globalThis.process.env.VERCEL_PRODUCTION_BRANCH_URL?.trim()); +export async function action({ request }: ActionFunctionArgs) { + const globalEnv = globalThis.process.env; + const projectId = globalEnv.VERCEL_PROJECT_ID; + const teamId = globalEnv.VERCEL_TEAM_ID; + const productionBranchUrl = globalEnv.VERCEL_PRODUCTION_BRANCH_URL || 'chef.convex.dev'; - console.log('globalThis.process.env', globalThis.process.env); + if (request.method !== 'POST') { + return new Response(JSON.stringify({ error: 'Method not allowed' }), { + status: 405, + headers: { 'Content-Type': 'application/json' }, + }); + } - if (!globalThis.process.env.VERCEL_TOKEN) { + if (!globalEnv.VERCEL_TOKEN) { return json({ error: 'Failed to fetch version information' }, { status: 500 }); } @@ -64,9 +58,7 @@ async function fetchVersionInfo() { // Even though we retrieved the production data, we might be on a preview branch deployment. // So, fetch the data specific to the latest branch deployment. const branchUrl = globalThis.process.env.VERCEL_BRANCH_URL || productionBranchUrl; - console.log('branchUrl', branchUrl); if (!branchUrl) { - console.log('branchUrl not set'); throw new Error('VERCEL_BRANCH_URL or VERCEL_PRODUCTION_BRANCH_URL not set'); } const branchResponse = await fetch( @@ -74,12 +66,10 @@ async function fetchVersionInfo() { requestOptions, ); if (!branchResponse.ok) { - console.log('branchResponse not ok'); throw new Error('Failed to fetch branch version information'); } const branchData = await branchResponse.json(); - console.log('branchData', branchData); return json( { @@ -87,15 +77,6 @@ async function fetchVersionInfo() { }, { status: 200 }, ); -} - -export async function action({ request }: ActionFunctionArgs) { - if (request.method !== 'POST') { - return new Response(JSON.stringify({ error: 'Method not allowed' }), { - status: 405, - headers: { 'Content-Type': 'application/json' }, - }); - } return await fetchVersionInfo(); } diff --git a/eslint.config.mjs b/eslint.config.mjs index ee69eab06..f8a5f702c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -6,6 +6,34 @@ import reactPlugin from 'eslint-plugin-react'; import reactHooksPlugin from 'eslint-plugin-react-hooks'; import noGlobalFetchRule from './eslint-rules/no-global-fetch.js'; +const noDirectProcessEnv = { + meta: { + type: "problem", + docs: { + description: "Disallow direct process.env usage", + category: "Best Practices", + }, + fixable: null, + schema: [], + messages: { + noDirectProcessEnv: + "Direct process.env usage is not allowed. Use a config module instead.", + }, + }, + create(context) { + return { + MemberExpression(node) { + if (node.object.name === "process" && node.property.name === "env") { + context.report({ + node, + messageId: "noDirectProcessEnv", + }); + } + }, + }; + }, +}; + export default [ { ignores: [ @@ -24,6 +52,11 @@ export default [ plugins: { react: reactPlugin, 'react-hooks': reactHooksPlugin, + local: { + rules: { + "no-direct-process-env": noDirectProcessEnv, + }, + }, }, rules: { ...reactPlugin.configs.flat.recommended.rules, @@ -84,6 +117,8 @@ export default [ selector: 'Literal[value=/bottom-4(?:\\D|$)/i]', }, ], + // Don't allow direct process.env usage + "local/no-direct-process-env": "error", }, settings: { react: { diff --git a/template/eslint.config.js b/template/eslint.config.js deleted file mode 100644 index dc2e21955..000000000 --- a/template/eslint.config.js +++ /dev/null @@ -1,77 +0,0 @@ -import js from "@eslint/js"; -import globals from "globals"; -import reactHooks from "eslint-plugin-react-hooks"; -import reactRefresh from "eslint-plugin-react-refresh"; -import tseslint from "typescript-eslint"; - -export default tseslint.config( - { - ignores: [ - "dist", - "eslint.config.js", - "convex/_generated", - "postcss.config.js", - "tailwind.config.js", - "vite.config.ts", - ], - }, - { - extends: [ - js.configs.recommended, - ...tseslint.configs.recommendedTypeChecked, - ], - files: ["**/*.{ts,tsx}"], - languageOptions: { - ecmaVersion: 2020, - globals: { - ...globals.browser, - ...globals.node, - }, - parserOptions: { - project: [ - "./tsconfig.node.json", - "./tsconfig.app.json", - "./convex/tsconfig.json", - ], - }, - }, - plugins: { - "react-hooks": reactHooks, - "react-refresh": reactRefresh, - }, - rules: { - ...reactHooks.configs.recommended.rules, - "react-refresh/only-export-components": [ - "warn", - { allowConstantExport: true }, - ], - // All of these overrides ease getting into - // TypeScript, and can be removed for stricter - // linting down the line. - - // Only warn on unused variables, and ignore variables starting with `_` - "@typescript-eslint/no-unused-vars": [ - "warn", - { varsIgnorePattern: "^_", argsIgnorePattern: "^_" }, - ], - - // Allow escaping the compiler - "@typescript-eslint/ban-ts-comment": "error", - - // Allow explicit `any`s - "@typescript-eslint/no-explicit-any": "off", - - // START: Allow implicit `any`s - "@typescript-eslint/no-unsafe-argument": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/no-unsafe-return": "off", - // END: Allow implicit `any`s - - // Allow async functions without await - // for consistency (esp. Convex `handler`s) - "@typescript-eslint/require-await": "off", - }, - }, -); From f0d4ebdf8637218e4f2618635516d8298795359f Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Fri, 18 Jul 2025 14:10:17 -0700 Subject: [PATCH 19/29] swr --- app/components/VersionNotificationBanner.tsx | 61 +++++++++----------- app/entry.client.tsx | 2 +- app/root.tsx | 2 +- 3 files changed, 30 insertions(+), 35 deletions(-) diff --git a/app/components/VersionNotificationBanner.tsx b/app/components/VersionNotificationBanner.tsx index 460487ea1..f9030b713 100644 --- a/app/components/VersionNotificationBanner.tsx +++ b/app/components/VersionNotificationBanner.tsx @@ -1,44 +1,25 @@ -import { useEffect, useState } from 'react'; import { toast } from 'sonner'; import { Button } from '@ui/Button'; import { SymbolIcon } from '@radix-ui/react-icons'; +import { captureMessage } from '@sentry/remix'; +import useSWR from 'swr'; export default function useVersionNotificationBanner() { - // @eslint-disable-next-line local/no-direct-process-env + // eslint-disable-next-line local/no-direct-process-env const currentSha = process.env.VERCEL_GIT_COMMIT_SHA; - const [productionSha, setProductionSha] = useState(''); - const [error, setError] = useState(false); + const { data, error } = useSWR<{ sha?: string | null }>('/api/version', { + // Refresh every hour. + refreshInterval: 1000 * 60 * 60, + // Refresh on focus at most every 10 minutes. + focusThrottleInterval: 1000 * 60 * 10, + shouldRetryOnError: false, + fetcher: versionFetcher, + }); - console.log('currentSha', currentSha); + console.log('data', data); + console.log('error', error); - useEffect(() => { - async function getVersion() { - try { - const res = await fetch('/api/version', { - method: 'POST', - }); - console.log('res', res); - if (!res.ok) { - throw new Error('Failed to fetch version information'); - } else { - const data = await res.json(); - setProductionSha(data.sha); - console.log('productionSha', productionSha); - } - } catch (_e) { - setError(true); - } - } - getVersion(); - - // TODO: Change to 60 * 60 * 1000 - const interval = setInterval(getVersion, 1000); - - // Cleanup interval on unmount - return () => clearInterval(interval); - }, [productionSha]); - - if (!error && productionSha && currentSha && productionSha !== currentSha) { + if (!error && data?.sha && currentSha && data.sha !== currentSha) { toast.info(
A new version of Chef is available! Refresh this page to update. @@ -60,3 +41,17 @@ export default function useVersionNotificationBanner() { ); } } + +const versionFetcher = async (url: string) => { + const res = await fetch(url); + if (!res.ok) { + try { + const { error } = await res.json(); + captureMessage(error); + } catch (_e) { + captureMessage('Failed to fetch dashboard version information.'); + } + throw new Error('Failed to fetch dashboard version information.'); + } + return res.json(); +}; diff --git a/app/entry.client.tsx b/app/entry.client.tsx index befbd4153..af00e976e 100644 --- a/app/entry.client.tsx +++ b/app/entry.client.tsx @@ -3,7 +3,7 @@ import { RemixBrowser, useLocation, useMatches } from '@remix-run/react'; import { startTransition, useEffect } from 'react'; import { hydrateRoot } from 'react-dom/client'; -// @eslint-disable-next-line local/no-direct-process-env +// eslint-disable-next-line local/no-direct-process-env const environment = process.env.VERCEL_ENV === 'production' ? 'production' : 'development'; Sentry.init({ diff --git a/app/root.tsx b/app/root.tsx index 0d9a555d5..a528049e2 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -25,7 +25,7 @@ import useVersionNotificationBanner from './components/VersionNotificationBanner export async function loader() { // These environment variables are available in the client (they aren't secret). - // @eslint-disable-next-line local/no-direct-process-env + // eslint-disable-next-line local/no-direct-process-env const CONVEX_URL = process.env.VITE_CONVEX_URL || globalThis.process.env.CONVEX_URL!; const CONVEX_OAUTH_CLIENT_ID = globalThis.process.env.CONVEX_OAUTH_CLIENT_ID!; return json({ From f6da5550de5865236a960b6bafd17effc6fff287 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Fri, 18 Jul 2025 14:10:23 -0700 Subject: [PATCH 20/29] deps --- package.json | 3 ++- pnpm-lock.yaml | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 30d69c1a8..63762a2cc 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "dependencies": { "@ai-sdk/amazon-bedrock": "^2.2.9", "@ai-sdk/anthropic": "^1.2.12", - "@convex-dev/ai-sdk-google": "1.2.17", "@ai-sdk/google": "^1.2.11", "@ai-sdk/openai": "^1.3.6", "@ai-sdk/react": "^1.2.5", @@ -57,6 +56,7 @@ "@codemirror/search": "^6.5.8", "@codemirror/state": "^6.4.1", "@codemirror/view": "^6.35.0", + "@convex-dev/ai-sdk-google": "1.2.17", "@convex-dev/design-system": "0.1.11", "@convex-dev/eslint-plugin": "0.0.1-alpha.4", "@convex-dev/migrations": "^0.2.8", @@ -135,6 +135,7 @@ "remix-utils": "^7.7.0", "shiki": "^1.24.0", "sonner": "^2.0.3", + "swr": "^2.3.4", "ua-parser-js": "^1.0.40", "undici": "^7.7.0", "unist-util-visit": "^5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c432d121..c6f70f8b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -325,6 +325,9 @@ importers: sonner: specifier: ^2.0.3 version: 2.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + swr: + specifier: ^2.3.4 + version: 2.3.4(react@18.3.1) ua-parser-js: specifier: ^1.0.40 version: 1.0.40 @@ -8657,6 +8660,11 @@ packages: peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + swr@2.3.4: + resolution: {integrity: sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + swrev@4.0.0: resolution: {integrity: sha512-LqVcOHSB4cPGgitD1riJ1Hh4vdmITOp+BkmfmXRh4hSF/t7EnS4iD+SOTmq7w5pPm/SiPeto4ADbKS6dHUDWFA==} @@ -9620,7 +9628,7 @@ snapshots: '@ai-sdk/provider-utils': 2.2.4(zod@3.24.1) '@ai-sdk/ui-utils': 1.2.5(zod@3.24.1) react: 18.3.1 - swr: 2.3.2(react@18.3.1) + swr: 2.3.4(react@18.3.1) throttleit: 2.1.0 optionalDependencies: zod: 3.24.1 @@ -18982,6 +18990,12 @@ snapshots: react: 18.3.1 use-sync-external-store: 1.4.0(react@18.3.1) + swr@2.3.4(react@18.3.1): + dependencies: + dequal: 2.0.3 + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + swrev@4.0.0: {} swrv@1.1.0(vue@3.5.13(typescript@5.8.3)): From 3251796a289ca763b833d8b44a10cd1aaf506125 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Fri, 18 Jul 2025 14:15:33 -0700 Subject: [PATCH 21/29] formatting --- eslint.config.mjs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index f8a5f702c..5da070228 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -8,25 +8,24 @@ import noGlobalFetchRule from './eslint-rules/no-global-fetch.js'; const noDirectProcessEnv = { meta: { - type: "problem", + type: 'problem', docs: { - description: "Disallow direct process.env usage", - category: "Best Practices", + description: 'Disallow direct process.env usage', + category: 'Best Practices', }, fixable: null, schema: [], messages: { - noDirectProcessEnv: - "Direct process.env usage is not allowed. Use a config module instead.", + noDirectProcessEnv: 'Direct process.env usage is not allowed. Use a config module instead.', }, }, create(context) { return { MemberExpression(node) { - if (node.object.name === "process" && node.property.name === "env") { + if (node.object.name === 'process' && node.property.name === 'env') { context.report({ node, - messageId: "noDirectProcessEnv", + messageId: 'noDirectProcessEnv', }); } }, @@ -54,7 +53,7 @@ export default [ 'react-hooks': reactHooksPlugin, local: { rules: { - "no-direct-process-env": noDirectProcessEnv, + 'no-direct-process-env': noDirectProcessEnv, }, }, }, @@ -118,7 +117,7 @@ export default [ }, ], // Don't allow direct process.env usage - "local/no-direct-process-env": "error", + 'local/no-direct-process-env': 'error', }, settings: { react: { From 74759a663790255a1da10169ff49e42a66cfc1ea Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Fri, 18 Jul 2025 14:17:37 -0700 Subject: [PATCH 22/29] remove console.log --- app/components/VersionNotificationBanner.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/components/VersionNotificationBanner.tsx b/app/components/VersionNotificationBanner.tsx index f9030b713..e8fba0640 100644 --- a/app/components/VersionNotificationBanner.tsx +++ b/app/components/VersionNotificationBanner.tsx @@ -16,9 +16,6 @@ export default function useVersionNotificationBanner() { fetcher: versionFetcher, }); - console.log('data', data); - console.log('error', error); - if (!error && data?.sha && currentSha && data.sha !== currentSha) { toast.info(
From be786fa8af33d7985d5e8b39811445d9f5777a1d Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Fri, 18 Jul 2025 14:21:56 -0700 Subject: [PATCH 23/29] nit --- app/routes/api.version.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/routes/api.version.ts b/app/routes/api.version.ts index 9238c8846..68aa80128 100644 --- a/app/routes/api.version.ts +++ b/app/routes/api.version.ts @@ -77,6 +77,4 @@ export async function action({ request }: ActionFunctionArgs) { }, { status: 200 }, ); - - return await fetchVersionInfo(); } From 4a980e996980fdf00f9c54e0925045299061a84d Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Fri, 18 Jul 2025 14:30:32 -0700 Subject: [PATCH 24/29] more testing --- app/components/VersionNotificationBanner.tsx | 9 +++++++-- app/routes/api.version.ts | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/components/VersionNotificationBanner.tsx b/app/components/VersionNotificationBanner.tsx index e8fba0640..084762237 100644 --- a/app/components/VersionNotificationBanner.tsx +++ b/app/components/VersionNotificationBanner.tsx @@ -9,13 +9,18 @@ export default function useVersionNotificationBanner() { const currentSha = process.env.VERCEL_GIT_COMMIT_SHA; const { data, error } = useSWR<{ sha?: string | null }>('/api/version', { // Refresh every hour. - refreshInterval: 1000 * 60 * 60, + refreshInterval: 1000 * 10, // Refresh on focus at most every 10 minutes. - focusThrottleInterval: 1000 * 60 * 10, + focusThrottleInterval: 1000 * 10, shouldRetryOnError: false, fetcher: versionFetcher, }); + console.log('data', data); + console.log('error', error); + console.log('currentSha', currentSha); + console.log('data?.sha', data?.sha); + if (!error && data?.sha && currentSha && data.sha !== currentSha) { toast.info(
diff --git a/app/routes/api.version.ts b/app/routes/api.version.ts index 68aa80128..f578632e2 100644 --- a/app/routes/api.version.ts +++ b/app/routes/api.version.ts @@ -7,6 +7,11 @@ export async function action({ request }: ActionFunctionArgs) { const teamId = globalEnv.VERCEL_TEAM_ID; const productionBranchUrl = globalEnv.VERCEL_PRODUCTION_BRANCH_URL || 'chef.convex.dev'; + console.log('globalEnv', globalEnv); + console.log('projectId', projectId); + console.log('teamId', teamId); + console.log('productionBranchUrl', productionBranchUrl); + if (request.method !== 'POST') { return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, @@ -15,6 +20,7 @@ export async function action({ request }: ActionFunctionArgs) { } if (!globalEnv.VERCEL_TOKEN) { + console.log('VERCEL_TOKEN not set'); return json({ error: 'Failed to fetch version information' }, { status: 500 }); } @@ -35,22 +41,27 @@ export async function action({ request }: ActionFunctionArgs) { `https://vercel.com/api/v1/projects/${projectId}/production-deployment?teamId=${teamId}`, requestOptions, ); + console.log('prodResponse', prodResponse); if (!prodResponse.ok) { + console.log('prodResponse not ok'); return json({ error: 'Failed to fetch production version information' }, { status: 500 }); } const prodData = await prodResponse.json(); + console.log('prodData', prodData); // Since we retrieved data from an undocumented API // let's defensively check that the data we need is present // and return an opaque error if it isn't. if (!prodData || typeof prodData.deploymentIsStale !== 'boolean') { + console.log('prodData not ok'); return json({ error: 'Failed to fetch production deployment' }, { status: 500 }); } // If the production deployment is rolled back, // we should not show a version notification. if (prodData.deploymentIsStale) { + console.log('prodData.deploymentIsStale', prodData.deploymentIsStale); return json({ sha: null }, { status: 200 }); } } @@ -59,6 +70,7 @@ export async function action({ request }: ActionFunctionArgs) { // So, fetch the data specific to the latest branch deployment. const branchUrl = globalThis.process.env.VERCEL_BRANCH_URL || productionBranchUrl; if (!branchUrl) { + console.log('branchUrl not set'); throw new Error('VERCEL_BRANCH_URL or VERCEL_PRODUCTION_BRANCH_URL not set'); } const branchResponse = await fetch( @@ -66,10 +78,12 @@ export async function action({ request }: ActionFunctionArgs) { requestOptions, ); if (!branchResponse.ok) { + console.log('branchResponse not ok'); throw new Error('Failed to fetch branch version information'); } const branchData = await branchResponse.json(); + console.log('branchData', branchData); return json( { From 93b6efb126996e73588c1038ef04d1af612a4e1d Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Fri, 18 Jul 2025 14:37:41 -0700 Subject: [PATCH 25/29] more testing --- app/components/VersionNotificationBanner.tsx | 4 +++- app/routes/api.version.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/components/VersionNotificationBanner.tsx b/app/components/VersionNotificationBanner.tsx index 084762237..e782e6419 100644 --- a/app/components/VersionNotificationBanner.tsx +++ b/app/components/VersionNotificationBanner.tsx @@ -45,7 +45,9 @@ export default function useVersionNotificationBanner() { } const versionFetcher = async (url: string) => { - const res = await fetch(url); + const res = await fetch(url, { + method: 'POST', + }); if (!res.ok) { try { const { error } = await res.json(); diff --git a/app/routes/api.version.ts b/app/routes/api.version.ts index f578632e2..708cc2221 100644 --- a/app/routes/api.version.ts +++ b/app/routes/api.version.ts @@ -84,6 +84,7 @@ export async function action({ request }: ActionFunctionArgs) { const branchData = await branchResponse.json(); console.log('branchData', branchData); + console.log('branchData.gitSource.sha', branchData.gitSource.sha); return json( { From 7bd0e09aca0878bdbddbda2afa38a22454df5109 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Fri, 18 Jul 2025 14:38:00 -0700 Subject: [PATCH 26/29] more logging --- app/components/VersionNotificationBanner.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/VersionNotificationBanner.tsx b/app/components/VersionNotificationBanner.tsx index e782e6419..8289f6f2f 100644 --- a/app/components/VersionNotificationBanner.tsx +++ b/app/components/VersionNotificationBanner.tsx @@ -48,6 +48,7 @@ const versionFetcher = async (url: string) => { const res = await fetch(url, { method: 'POST', }); + console.log('res', res); if (!res.ok) { try { const { error } = await res.json(); From 8000f6e20fbdce468d82435a3813acfe36a9d58b Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Fri, 18 Jul 2025 14:43:32 -0700 Subject: [PATCH 27/29] remove logs --- app/components/VersionNotificationBanner.tsx | 11 +++-------- app/routes/api.version.ts | 15 --------------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/app/components/VersionNotificationBanner.tsx b/app/components/VersionNotificationBanner.tsx index 8289f6f2f..a54121329 100644 --- a/app/components/VersionNotificationBanner.tsx +++ b/app/components/VersionNotificationBanner.tsx @@ -9,18 +9,13 @@ export default function useVersionNotificationBanner() { const currentSha = process.env.VERCEL_GIT_COMMIT_SHA; const { data, error } = useSWR<{ sha?: string | null }>('/api/version', { // Refresh every hour. - refreshInterval: 1000 * 10, + refreshInterval: 1000 * 60 * 60, // Refresh on focus at most every 10 minutes. - focusThrottleInterval: 1000 * 10, + focusThrottleInterval: 1000 * 60 * 10, shouldRetryOnError: false, fetcher: versionFetcher, }); - console.log('data', data); - console.log('error', error); - console.log('currentSha', currentSha); - console.log('data?.sha', data?.sha); - if (!error && data?.sha && currentSha && data.sha !== currentSha) { toast.info(
@@ -48,7 +43,7 @@ const versionFetcher = async (url: string) => { const res = await fetch(url, { method: 'POST', }); - console.log('res', res); + if (!res.ok) { try { const { error } = await res.json(); diff --git a/app/routes/api.version.ts b/app/routes/api.version.ts index 708cc2221..68aa80128 100644 --- a/app/routes/api.version.ts +++ b/app/routes/api.version.ts @@ -7,11 +7,6 @@ export async function action({ request }: ActionFunctionArgs) { const teamId = globalEnv.VERCEL_TEAM_ID; const productionBranchUrl = globalEnv.VERCEL_PRODUCTION_BRANCH_URL || 'chef.convex.dev'; - console.log('globalEnv', globalEnv); - console.log('projectId', projectId); - console.log('teamId', teamId); - console.log('productionBranchUrl', productionBranchUrl); - if (request.method !== 'POST') { return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, @@ -20,7 +15,6 @@ export async function action({ request }: ActionFunctionArgs) { } if (!globalEnv.VERCEL_TOKEN) { - console.log('VERCEL_TOKEN not set'); return json({ error: 'Failed to fetch version information' }, { status: 500 }); } @@ -41,27 +35,22 @@ export async function action({ request }: ActionFunctionArgs) { `https://vercel.com/api/v1/projects/${projectId}/production-deployment?teamId=${teamId}`, requestOptions, ); - console.log('prodResponse', prodResponse); if (!prodResponse.ok) { - console.log('prodResponse not ok'); return json({ error: 'Failed to fetch production version information' }, { status: 500 }); } const prodData = await prodResponse.json(); - console.log('prodData', prodData); // Since we retrieved data from an undocumented API // let's defensively check that the data we need is present // and return an opaque error if it isn't. if (!prodData || typeof prodData.deploymentIsStale !== 'boolean') { - console.log('prodData not ok'); return json({ error: 'Failed to fetch production deployment' }, { status: 500 }); } // If the production deployment is rolled back, // we should not show a version notification. if (prodData.deploymentIsStale) { - console.log('prodData.deploymentIsStale', prodData.deploymentIsStale); return json({ sha: null }, { status: 200 }); } } @@ -70,7 +59,6 @@ export async function action({ request }: ActionFunctionArgs) { // So, fetch the data specific to the latest branch deployment. const branchUrl = globalThis.process.env.VERCEL_BRANCH_URL || productionBranchUrl; if (!branchUrl) { - console.log('branchUrl not set'); throw new Error('VERCEL_BRANCH_URL or VERCEL_PRODUCTION_BRANCH_URL not set'); } const branchResponse = await fetch( @@ -78,13 +66,10 @@ export async function action({ request }: ActionFunctionArgs) { requestOptions, ); if (!branchResponse.ok) { - console.log('branchResponse not ok'); throw new Error('Failed to fetch branch version information'); } const branchData = await branchResponse.json(); - console.log('branchData', branchData); - console.log('branchData.gitSource.sha', branchData.gitSource.sha); return json( { From 9516e818c7d2722fd0c2e161ee048845f2f534be Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Fri, 18 Jul 2025 14:47:22 -0700 Subject: [PATCH 28/29] copy --- eslint.config.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 5da070228..054e966e3 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -16,7 +16,8 @@ const noDirectProcessEnv = { fixable: null, schema: [], messages: { - noDirectProcessEnv: 'Direct process.env usage is not allowed. Use a config module instead.', + noDirectProcessEnv: + 'Direct process.env usage is not allowed. Use globalThis.process.env instead because process.env is shimmed in for both the browser and the server.', }, }, create(context) { From cf13c65b9661146c0d1face168bd5f9438ed5981 Mon Sep 17 00:00:00 2001 From: Jordan Hunt Date: Fri, 18 Jul 2025 14:49:41 -0700 Subject: [PATCH 29/29] delete a console.log --- app/routes/api.enhance-prompt.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/routes/api.enhance-prompt.ts b/app/routes/api.enhance-prompt.ts index 60acb220c..e4f2ee017 100644 --- a/app/routes/api.enhance-prompt.ts +++ b/app/routes/api.enhance-prompt.ts @@ -130,8 +130,6 @@ export async function action({ request }: ActionFunctionArgs) { apiKey: globalThis.process.env.OPENAI_API_KEY, }); - console.log('openai.apiKey', openai.apiKey); - const completion = await openai.chat.completions.create({ model: 'gpt-4.1-mini', messages: [