fix: demo-banner (#305)

This commit is contained in:
Charles GTE
2026-06-02 18:36:35 +02:00
committed by GitHub
parent 564dee512f
commit 09791ae86e
5 changed files with 77 additions and 12 deletions
+2 -2
View File
@@ -11,8 +11,8 @@ import { env } from "@/env.mjs";
* Auth is handled by withApiKey before MCP is ever touched. * Auth is handled by withApiKey before MCP is ever touched.
* The validated key is forwarded by MCP tools to downstream /api/v1 REST calls. * The validated key is forwarded by MCP tools to downstream /api/v1 REST calls.
*/ */
const apiEnabled = String(env.API_ENABLED) === "true"; const apiEnabled = env.API_ENABLED;
const mcpEnabled = String(env.MCP_ENABLED) === "true"; const mcpEnabled = env.MCP_ENABLED;
export const POST = apiEnabled && mcpEnabled export const POST = apiEnabled && mcpEnabled
? withApiKey(async (req: Request, ctx: ApiKeyContext) => { ? withApiKey(async (req: Request, ctx: ApiKeyContext) => {
+2 -2
View File
@@ -43,7 +43,7 @@ export async function proxy(request: NextRequest) {
if (url.pathname.startsWith("/api")) { if (url.pathname.startsWith("/api")) {
if (url.pathname.startsWith("/api/v1")) { if (url.pathname.startsWith("/api/v1")) {
const apiEnabled = String(env.API_ENABLED) === "true"; const apiEnabled = env.API_ENABLED;
if (!apiEnabled) { if (!apiEnabled) {
return new NextResponse( return new NextResponse(
JSON.stringify({ JSON.stringify({
@@ -53,7 +53,7 @@ export async function proxy(request: NextRequest) {
{ status: 404, headers: { "Content-Type": "application/json" } }, { status: 404, headers: { "Content-Type": "application/json" } },
); );
} }
const openapiEnabled = String(env.OPENAPI_ENABLED) === "true"; const openapiEnabled = env.OPENAPI_ENABLED;
if ( if (
!openapiEnabled && !openapiEnabled &&
(url.pathname.startsWith("/api/v1/docs") || (url.pathname.startsWith("/api/v1/docs") ||
+15 -8
View File
@@ -36,8 +36,8 @@ export const env = createEnv({
SMTP_SECURE: z SMTP_SECURE: z
.enum(["true", "false"]) .enum(["true", "false"])
.transform((val) => val === "true") .default("true")
.default("true"), .transform((val) => val === "true"),
AUTH_GOOGLE_ID: z.string().optional(), AUTH_GOOGLE_ID: z.string().optional(),
AUTH_GOOGLE_SECRET: z.string().optional(), AUTH_GOOGLE_SECRET: z.string().optional(),
@@ -101,18 +101,23 @@ export const env = createEnv({
OPENAPI_ENABLED: z OPENAPI_ENABLED: z
.enum(["true", "false"]) .enum(["true", "false"])
.transform((val) => val === "true") .default("false")
.default("false"), .transform((val) => val === "true"),
API_ENABLED: z API_ENABLED: z
.enum(["true", "false"]) .enum(["true", "false"])
.transform((val) => val === "true") .default("false")
.default("false"), .transform((val) => val === "true"),
MCP_ENABLED: z MCP_ENABLED: z
.enum(["true", "false"]) .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: { client: {
NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(), NEXT_PUBLIC_PROJECT_VERSION: z.string().optional(),
@@ -190,5 +195,7 @@ export const env = createEnv({
API_ENABLED: process.env.API_ENABLED, API_ENABLED: process.env.API_ENABLED,
MCP_ENABLED: process.env.MCP_ENABLED, MCP_ENABLED: process.env.MCP_ENABLED,
DEMO_ENABLED: process.env.DEMO_ENABLED,
}, },
}); });
+54
View File
@@ -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<number | null>(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 (
<Badge
variant="outline"
className={cn(
'hidden md:inline-flex h-7',
isResetting && 'animate-pulse border-red-200 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-950/30 dark:text-red-400',
isWarning && 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-400',
)}
>
<Timer aria-hidden="true" />
{isResetting ? 'Resetting…' : `Demo resets in ${formatTime(secondsLeft)}`}
</Badge>
);
}
+4
View File
@@ -5,12 +5,15 @@ import {currentUser} from "@/lib/auth/current-user";
import {BreadCrumbsWrapper} from "@/components/common/bread-crumbs"; import {BreadCrumbsWrapper} from "@/components/common/bread-crumbs";
import GitHubStarsButtonCustom from "@/components/common/github-button"; import GitHubStarsButtonCustom from "@/components/common/github-button";
import {LoggedInButton} from "@/features/layout/logged-in-button.server"; 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 } = {}) => { export const Header = async ({ actions }: { actions?: ReactNode } = {}) => {
const user = await currentUser(); const user = await currentUser();
if (!user) { if (!user) {
return notFound(); return notFound();
} }
const demoEnabled = env.DEMO_ENABLED;
return ( return (
<header className="flex h-16 shrink-0 items-center justify-between border-b px-4"> <header className="flex h-16 shrink-0 items-center justify-between border-b px-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -18,6 +21,7 @@ export const Header = async ({ actions }: { actions?: ReactNode } = {}) => {
<BreadCrumbsWrapper/> <BreadCrumbsWrapper/>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{demoEnabled && <DemoResetBanner />}
<GitHubStarsButtonCustom/> <GitHubStarsButtonCustom/>
{actions} {actions}
<LoggedInButton/> <LoggedInButton/>