diff --git a/app/api/v1/mcp/route.ts b/app/api/v1/mcp/route.ts index 52b77d86..0324637d 100644 --- a/app/api/v1/mcp/route.ts +++ b/app/api/v1/mcp/route.ts @@ -11,8 +11,8 @@ import { env } from "@/env.mjs"; * Auth is handled by withApiKey before MCP is ever touched. * The validated key is forwarded by MCP tools to downstream /api/v1 REST calls. */ -const apiEnabled = String(env.API_ENABLED) === "true"; -const mcpEnabled = String(env.MCP_ENABLED) === "true"; +const apiEnabled = env.API_ENABLED; +const mcpEnabled = env.MCP_ENABLED; export const POST = apiEnabled && mcpEnabled ? withApiKey(async (req: Request, ctx: ApiKeyContext) => { diff --git a/proxy.ts b/proxy.ts index d4dbd345..32331a12 100644 --- a/proxy.ts +++ b/proxy.ts @@ -43,7 +43,7 @@ export async function proxy(request: NextRequest) { if (url.pathname.startsWith("/api")) { if (url.pathname.startsWith("/api/v1")) { - const apiEnabled = String(env.API_ENABLED) === "true"; + const apiEnabled = env.API_ENABLED; if (!apiEnabled) { return new NextResponse( JSON.stringify({ @@ -53,7 +53,7 @@ export async function proxy(request: NextRequest) { { status: 404, headers: { "Content-Type": "application/json" } }, ); } - const openapiEnabled = String(env.OPENAPI_ENABLED) === "true"; + const openapiEnabled = env.OPENAPI_ENABLED; if ( !openapiEnabled && (url.pathname.startsWith("/api/v1/docs") || diff --git a/src/env.mjs b/src/env.mjs index 5c3e4b43..46155d10 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -36,8 +36,8 @@ export const env = createEnv({ SMTP_SECURE: z .enum(["true", "false"]) - .transform((val) => val === "true") - .default("true"), + .default("true") + .transform((val) => val === "true"), AUTH_GOOGLE_ID: z.string().optional(), AUTH_GOOGLE_SECRET: z.string().optional(), @@ -101,18 +101,23 @@ export const env = createEnv({ OPENAPI_ENABLED: z .enum(["true", "false"]) - .transform((val) => val === "true") - .default("false"), + .default("false") + .transform((val) => val === "true"), API_ENABLED: z .enum(["true", "false"]) - .transform((val) => val === "true") - .default("false"), + .default("false") + .transform((val) => val === "true"), MCP_ENABLED: z .enum(["true", "false"]) - .transform((val) => val === "true") - .default("false"), + .default("false") + .transform((val) => val === "true"), + + DEMO_ENABLED: z + .enum(["true", "false"]) + .default("false") + .transform((val) => val === "true"), }, client: { NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(), @@ -190,5 +195,7 @@ export const env = createEnv({ API_ENABLED: process.env.API_ENABLED, MCP_ENABLED: process.env.MCP_ENABLED, + DEMO_ENABLED: process.env.DEMO_ENABLED, + }, }); diff --git a/src/features/layout/demo-reset-banner.tsx b/src/features/layout/demo-reset-banner.tsx new file mode 100644 index 00000000..b58ab7f7 --- /dev/null +++ b/src/features/layout/demo-reset-banner.tsx @@ -0,0 +1,54 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { Timer } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { Badge } from '@/components/ui/badge'; + +function getSecondsUntilNextHour(): number { + const now = new Date(); + const s = 3600 - (now.getMinutes() * 60 + now.getSeconds()); + return s === 3600 ? 0 : s; +} + +function formatTime(seconds: number): string { + const s = Math.max(0, seconds); + const m = Math.floor(s / 60); + const sec = s % 60; + return `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`; +} + +export function DemoResetBanner() { + const [secondsLeft, setSecondsLeft] = useState(null); + + useEffect(() => { + const tick = () => setSecondsLeft(getSecondsUntilNextHour()); + + const timeout = setTimeout(tick, 0); + const interval = setInterval(tick, 1000); + + return () => { + clearTimeout(timeout); + clearInterval(interval); + }; + }, []); + + if (secondsLeft === null) return null; + + const isResetting = secondsLeft === 0; + const isWarning = secondsLeft > 0 && secondsLeft <= 300; + + return ( + + + ); +} diff --git a/src/features/layout/header.tsx b/src/features/layout/header.tsx index ad7641c8..ed3d0f7b 100644 --- a/src/features/layout/header.tsx +++ b/src/features/layout/header.tsx @@ -5,12 +5,15 @@ import {currentUser} from "@/lib/auth/current-user"; import {BreadCrumbsWrapper} from "@/components/common/bread-crumbs"; import GitHubStarsButtonCustom from "@/components/common/github-button"; import {LoggedInButton} from "@/features/layout/logged-in-button.server"; +import { env } from "@/env.mjs"; +import { DemoResetBanner } from "@/features/layout/demo-reset-banner"; export const Header = async ({ actions }: { actions?: ReactNode } = {}) => { const user = await currentUser(); if (!user) { return notFound(); } + const demoEnabled = env.DEMO_ENABLED; return (
@@ -18,6 +21,7 @@ export const Header = async ({ actions }: { actions?: ReactNode } = {}) => {
+ {demoEnabled && } {actions}