ui/ health checklist
This commit is contained in:
126
src/app/api/diagnostics/route.ts
Normal file
126
src/app/api/diagnostics/route.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
type StatusLevel = "ok" | "warn" | "error";
|
||||
|
||||
interface DiagnosticsResponse {
|
||||
timestamp: string;
|
||||
env: {
|
||||
DATABASE_URL: boolean;
|
||||
BETTER_AUTH_SECRET: boolean;
|
||||
GOOGLE_CLIENT_ID: boolean;
|
||||
GOOGLE_CLIENT_SECRET: boolean;
|
||||
OPENAI_API_KEY: boolean;
|
||||
NEXT_PUBLIC_APP_URL: boolean;
|
||||
};
|
||||
database: {
|
||||
connected: boolean;
|
||||
schemaApplied: boolean;
|
||||
error?: string;
|
||||
};
|
||||
auth: {
|
||||
configured: boolean;
|
||||
routeResponding: boolean | null;
|
||||
};
|
||||
ai: {
|
||||
configured: boolean;
|
||||
};
|
||||
overallStatus: StatusLevel;
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const env = {
|
||||
DATABASE_URL: Boolean(process.env.DATABASE_URL),
|
||||
BETTER_AUTH_SECRET: Boolean(process.env.BETTER_AUTH_SECRET),
|
||||
GOOGLE_CLIENT_ID: Boolean(process.env.GOOGLE_CLIENT_ID),
|
||||
GOOGLE_CLIENT_SECRET: Boolean(process.env.GOOGLE_CLIENT_SECRET),
|
||||
OPENAI_API_KEY: Boolean(process.env.OPENAI_API_KEY),
|
||||
NEXT_PUBLIC_APP_URL: Boolean(process.env.NEXT_PUBLIC_APP_URL),
|
||||
} as const;
|
||||
|
||||
// Database checks
|
||||
let dbConnected = false;
|
||||
let schemaApplied = false;
|
||||
let dbError: string | undefined;
|
||||
if (env.DATABASE_URL) {
|
||||
try {
|
||||
const [{ db }, { sql }, schema] = await Promise.all([
|
||||
import("@/lib/db"),
|
||||
import("drizzle-orm"),
|
||||
import("@/lib/schema"),
|
||||
]);
|
||||
// Ping DB
|
||||
await db.execute(sql`select 1`);
|
||||
dbConnected = true;
|
||||
try {
|
||||
// Touch a known table to verify migrations
|
||||
await db.select().from(schema.user).limit(1);
|
||||
schemaApplied = true;
|
||||
} catch {
|
||||
schemaApplied = false;
|
||||
}
|
||||
} catch (err) {
|
||||
dbConnected = false;
|
||||
dbError = err instanceof Error ? err.message : "Unknown database error";
|
||||
}
|
||||
} else {
|
||||
dbConnected = false;
|
||||
schemaApplied = false;
|
||||
dbError = "DATABASE_URL is not set";
|
||||
}
|
||||
|
||||
// Auth route check: we consider the route responding if it returns any HTTP response
|
||||
// for /api/auth/session (status codes in the 2xx-4xx range are acceptable for readiness)
|
||||
const origin = (() => {
|
||||
try {
|
||||
return new URL(req.url).origin;
|
||||
} catch {
|
||||
return process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
||||
}
|
||||
})();
|
||||
|
||||
let authRouteResponding: boolean | null = null;
|
||||
try {
|
||||
const res = await fetch(`${origin}/api/auth/session`, {
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" },
|
||||
cache: "no-store",
|
||||
});
|
||||
authRouteResponding = res.status >= 200 && res.status < 500;
|
||||
} catch {
|
||||
authRouteResponding = false;
|
||||
}
|
||||
|
||||
const authConfigured =
|
||||
env.BETTER_AUTH_SECRET && env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET;
|
||||
const aiConfigured = env.OPENAI_API_KEY; // We avoid live-calling the AI provider here
|
||||
|
||||
const overallStatus: StatusLevel = (() => {
|
||||
if (!env.DATABASE_URL || !dbConnected || !schemaApplied) return "error";
|
||||
if (!authConfigured) return "error";
|
||||
// AI is optional; warn if not configured
|
||||
if (!aiConfigured) return "warn";
|
||||
return "ok";
|
||||
})();
|
||||
|
||||
const body: DiagnosticsResponse = {
|
||||
timestamp: new Date().toISOString(),
|
||||
env,
|
||||
database: {
|
||||
connected: dbConnected,
|
||||
schemaApplied,
|
||||
error: dbError,
|
||||
},
|
||||
auth: {
|
||||
configured: authConfigured,
|
||||
routeResponding: authRouteResponding,
|
||||
},
|
||||
ai: {
|
||||
configured: aiConfigured,
|
||||
},
|
||||
overallStatus,
|
||||
};
|
||||
|
||||
return NextResponse.json(body, {
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import { useSession } from "@/lib/auth-client";
|
||||
import { useState, type ReactNode } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import type { Components } from "react-markdown";
|
||||
import type { CodeComponent } from "react-markdown/lib/ast-to-react";
|
||||
|
||||
const H1: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = (props) => (
|
||||
<h1 className="mt-2 mb-3 text-2xl font-bold" {...props} />
|
||||
@@ -35,6 +34,7 @@ const Anchor: React.FC<React.AnchorHTMLAttributes<HTMLAnchorElement>> = (
|
||||
) => (
|
||||
<a
|
||||
className="underline underline-offset-2 text-primary hover:opacity-90"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
{...props}
|
||||
/>
|
||||
@@ -47,8 +47,11 @@ const Blockquote: React.FC<React.BlockquoteHTMLAttributes<HTMLElement>> = (
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
const Code: CodeComponent = ({ inline, children, ...props }) => {
|
||||
if (inline) {
|
||||
const Code: Components["code"] = ({ children, className, ...props }) => {
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
const isInline = !match;
|
||||
|
||||
if (isInline) {
|
||||
return (
|
||||
<code className="rounded bg-muted px-1 py-0.5 text-xs" {...props}>
|
||||
{children}
|
||||
@@ -116,11 +119,7 @@ function renderMessageContent(message: MaybePartsMessage): ReactNode {
|
||||
: [];
|
||||
return parts.map((p, idx) =>
|
||||
p?.type === "text" && p.text ? (
|
||||
<ReactMarkdown
|
||||
key={idx}
|
||||
linkTarget="_blank"
|
||||
components={markdownComponents}
|
||||
>
|
||||
<ReactMarkdown key={idx} components={markdownComponents}>
|
||||
{p.text}
|
||||
</ReactMarkdown>
|
||||
) : null
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SetupChecklist } from "@/components/setup-checklist";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
@@ -44,6 +45,8 @@ export default function Home() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 mt-12">
|
||||
<SetupChecklist />
|
||||
|
||||
<h3 className="text-2xl font-semibold">Next Steps</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-left">
|
||||
<div className="p-4 border rounded-lg">
|
||||
|
||||
Reference in New Issue
Block a user