From 0364b8e6b9a065c37ec1bcc4da22a10aba59d260 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:39:14 +0000 Subject: [PATCH] Changes --- src/App.tsx | 4 + src/components/dev/ApiDevTools.tsx | 267 +++++++++++++++++++++++++++++ 2 files changed, 271 insertions(+) create mode 100644 src/components/dev/ApiDevTools.tsx diff --git a/src/App.tsx b/src/App.tsx index 1aa0198..8017e78 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,6 +33,7 @@ import OrgAuditPage from "@/pages/org/OrgAuditPage"; import OIDCClientsPage from "@/pages/org/OIDCClientsPage"; import NotFound from "@/pages/NotFound"; +import ApiDevTools from "@/components/dev/ApiDevTools"; const queryClient = new QueryClient(); @@ -89,6 +90,9 @@ function AppRoutes() { {/* Catch-all */} } /> + + {/* Dev tools - only shown in development */} + ); } diff --git a/src/components/dev/ApiDevTools.tsx b/src/components/dev/ApiDevTools.tsx new file mode 100644 index 0000000..b17715f --- /dev/null +++ b/src/components/dev/ApiDevTools.tsx @@ -0,0 +1,267 @@ +import { useState, useEffect } from "react"; +import { ChevronUp, ChevronDown, X, Trash2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; + +interface ApiLog { + id: string; + timestamp: Date; + method: string; + url: string; + requestBody?: unknown; + status?: number; + responseBody?: unknown; + duration?: number; + error?: string; +} + +// Global store for API logs +const apiLogs: ApiLog[] = []; +const listeners: Set<() => void> = new Set(); + +function notifyListeners() { + listeners.forEach((fn) => fn()); +} + +export function addApiLog(log: ApiLog) { + apiLogs.unshift(log); + if (apiLogs.length > 50) apiLogs.pop(); + notifyListeners(); +} + +export function updateApiLog(id: string, updates: Partial) { + const log = apiLogs.find((l) => l.id === id); + if (log) { + Object.assign(log, updates); + notifyListeners(); + } +} + +function clearLogs() { + apiLogs.length = 0; + notifyListeners(); +} + +// Intercept fetch +const originalFetch = window.fetch; +window.fetch = async function (input, init) { + const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + + // Only log API calls + if (!url.includes("/api/")) { + return originalFetch.apply(this, [input, init]); + } + + const id = crypto.randomUUID(); + const method = init?.method || "GET"; + let requestBody: unknown; + + try { + if (init?.body) { + requestBody = JSON.parse(init.body as string); + } + } catch { + requestBody = init?.body; + } + + addApiLog({ + id, + timestamp: new Date(), + method, + url, + requestBody, + }); + + const start = performance.now(); + + try { + const response = await originalFetch.apply(this, [input, init]); + const duration = Math.round(performance.now() - start); + + // Clone response to read body + const cloned = response.clone(); + let responseBody: unknown; + try { + responseBody = await cloned.json(); + } catch { + responseBody = await cloned.text(); + } + + updateApiLog(id, { + status: response.status, + responseBody, + duration, + }); + + return response; + } catch (err) { + updateApiLog(id, { + error: err instanceof Error ? err.message : "Unknown error", + duration: Math.round(performance.now() - start), + }); + throw err; + } +}; + +export default function ApiDevTools() { + const [isOpen, setIsOpen] = useState(false); + const [logs, setLogs] = useState([...apiLogs]); + const [selectedLog, setSelectedLog] = useState(null); + + useEffect(() => { + const update = () => setLogs([...apiLogs]); + listeners.add(update); + return () => { + listeners.delete(update); + }; + }, []); + + const getStatusColor = (status?: number) => { + if (!status) return "secondary"; + if (status >= 200 && status < 300) return "default"; + if (status >= 400) return "destructive"; + return "secondary"; + }; + + if (!isOpen) { + return ( + + ); + } + + return ( +
+ {/* Header */} +
+
+ Gatehouse API DevTools + + {logs.length} requests + +
+
+ + +
+
+ +
+ {/* Request list */} + + {logs.length === 0 ? ( +
+ No API requests yet +
+ ) : ( +
+ {logs.map((log) => ( + + ))} +
+ )} +
+ + {/* Detail view */} + + {selectedLog ? ( +
+
+
URL
+
{selectedLog.url}
+
+ + {selectedLog.requestBody && ( +
+
Request Body
+
+                    {JSON.stringify(selectedLog.requestBody, null, 2)}
+                  
+
+ )} + + {selectedLog.responseBody && ( +
+
Response
+
+                    {JSON.stringify(selectedLog.responseBody, null, 2)}
+                  
+
+ )} + + {selectedLog.error && ( +
+
Error
+
{selectedLog.error}
+
+ )} +
+ ) : ( +
+ Select a request to view details +
+ )} +
+
+
+ ); +}