From 7bcb58e3db04734af2799fc7423226bffc4b367a Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 19 Mar 2026 11:11:07 -0300 Subject: [PATCH] feat(logs): add export button with time range dropdown (1h, 6h, 12h, 24h) - New API: /api/logs/export?hours=24&type=call-logs - UI: Export button with dropdown on /dashboard/logs page - Supports export of request-logs, proxy-logs, and call-logs - Downloads as JSON file with Content-Disposition header --- src/app/(dashboard)/dashboard/logs/page.tsx | 130 ++++++++++++++++++-- src/app/api/logs/export/route.ts | 58 +++++++++ 2 files changed, 177 insertions(+), 11 deletions(-) create mode 100644 src/app/api/logs/export/route.ts diff --git a/src/app/(dashboard)/dashboard/logs/page.tsx b/src/app/(dashboard)/dashboard/logs/page.tsx index e096a1a2c6..9df2d0ada2 100644 --- a/src/app/(dashboard)/dashboard/logs/page.tsx +++ b/src/app/(dashboard)/dashboard/logs/page.tsx @@ -1,27 +1,135 @@ "use client"; -import { useState } from "react"; +import { useState, useRef, useEffect } from "react"; import { RequestLoggerV2, ProxyLogger, SegmentedControl } from "@/shared/components"; import ConsoleLogViewer from "@/shared/components/ConsoleLogViewer"; import AuditLogTab from "./AuditLogTab"; import { useTranslations } from "next-intl"; +const TIME_RANGES = [ + { label: "1h", hours: 1 }, + { label: "6h", hours: 6 }, + { label: "12h", hours: 12 }, + { label: "24h", hours: 24 }, +]; + +const TAB_TO_LOG_TYPE: Record = { + "request-logs": "request-logs", + "proxy-logs": "proxy-logs", + "audit-logs": "call-logs", + console: "call-logs", +}; + export default function LogsPage() { const [activeTab, setActiveTab] = useState("request-logs"); + const [showExport, setShowExport] = useState(false); + const [exporting, setExporting] = useState(false); + const dropdownRef = useRef(null); const t = useTranslations("logs"); + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { + setShowExport(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + async function handleExport(hours: number) { + setExporting(true); + setShowExport(false); + try { + const logType = TAB_TO_LOG_TYPE[activeTab] || "call-logs"; + const res = await fetch(`/api/logs/export?hours=${hours}&type=${logType}`); + if (!res.ok) throw new Error("Export failed"); + const blob = await res.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `omniroute-${logType}-${hours}h-${new Date().toISOString().slice(0, 10)}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch (err) { + console.error("Export failed:", err); + } finally { + setExporting(false); + } + } + return (
- +
+ + +
+ + + {showExport && ( +
+
+ Time Range +
+ {TIME_RANGES.map((range) => ( + + ))} +
+ )} +
+
{/* Content */} {activeTab === "request-logs" && } diff --git a/src/app/api/logs/export/route.ts b/src/app/api/logs/export/route.ts new file mode 100644 index 0000000000..afb0a24c3c --- /dev/null +++ b/src/app/api/logs/export/route.ts @@ -0,0 +1,58 @@ +import { getDbInstance } from "@/lib/db/core"; + +/** + * GET /api/logs/export — export logs as JSON + * Query params: ?hours=24 (1, 6, 12, 24; default 24) + * &type=call-logs|request-logs|proxy-logs (default call-logs) + */ +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const hours = Math.min(Math.max(parseInt(searchParams.get("hours") || "24") || 24, 1), 168); + const logType = searchParams.get("type") || "call-logs"; + + const since = new Date(Date.now() - hours * 3600 * 1000).toISOString(); + const db = getDbInstance(); + + let rows: unknown[] = []; + let tableName = ""; + + if (logType === "call-logs") { + tableName = "call_logs"; + const stmt = db.prepare( + "SELECT * FROM call_logs WHERE timestamp >= @since ORDER BY timestamp DESC" + ); + rows = stmt.all({ since }); + } else if (logType === "request-logs") { + tableName = "request_logs"; + const stmt = db.prepare( + "SELECT * FROM request_logs WHERE timestamp >= @since ORDER BY timestamp DESC" + ); + rows = stmt.all({ since }); + } else if (logType === "proxy-logs") { + tableName = "proxy_logs"; + const stmt = db.prepare( + "SELECT * FROM proxy_logs WHERE timestamp >= @since ORDER BY timestamp DESC" + ); + rows = stmt.all({ since }); + } + + const filename = `omniroute-${tableName}-${hours}h-${new Date().toISOString().slice(0, 10)}.json`; + + return new Response( + JSON.stringify({ logs: rows, count: rows.length, hours, type: logType }, null, 2), + { + status: 200, + headers: { + "Content-Type": "application/json", + "Content-Disposition": `attachment; filename="${filename}"`, + }, + } + ); + } catch (error) { + return Response.json( + { error: { message: (error as Error).message, type: "server_error" } }, + { status: 500 } + ); + } +}