Repository navigation
Fix Request Logs clean history purge #3751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ec78fe3
229e1dc
6048610
0b5d9c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,20 @@ const TIME_RANGES = [ | |||||||||||||||||||||||||||||||||||||||||
| { label: "24h", hours: 24 }, | ||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| type LogsTranslator = ((key: string, values?: Record<string, unknown>) => string) & { | ||||||||||||||||||||||||||||||||||||||||||
| has?: (key: string) => boolean; | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| function logsText( | ||||||||||||||||||||||||||||||||||||||||||
| t: LogsTranslator, | ||||||||||||||||||||||||||||||||||||||||||
| key: string, | ||||||||||||||||||||||||||||||||||||||||||
| fallback: string, | ||||||||||||||||||||||||||||||||||||||||||
| values?: Record<string, unknown> | ||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||
| if (typeof t.has !== "function" || !t.has(key)) return fallback; | ||||||||||||||||||||||||||||||||||||||||||
| return values ? t(key, values) : t(key); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| export default function LogsPage() { | ||||||||||||||||||||||||||||||||||||||||||
| const [showExport, setShowExport] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||
| const [exporting, setExporting] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -21,7 +35,7 @@ export default function LogsPage() { | |||||||||||||||||||||||||||||||||||||||||
| const [requestLogKey, setRequestLogKey] = useState(0); | ||||||||||||||||||||||||||||||||||||||||||
| const dropdownRef = useRef<HTMLDivElement>(null); | ||||||||||||||||||||||||||||||||||||||||||
| const requestLoggerRef = useRef<any>(null); | ||||||||||||||||||||||||||||||||||||||||||
| const t = useTranslations("logs"); | ||||||||||||||||||||||||||||||||||||||||||
| const t = useTranslations("logs") as LogsTranslator; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||
| function handleClickOutside(e: MouseEvent) { | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -34,7 +48,8 @@ export default function LogsPage() { | |||||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // initial id from URL (synchronously on client) so child can open on mount | ||||||||||||||||||||||||||||||||||||||||||
| const initialId = typeof window !== "undefined" ? new URL(window.location.href).searchParams.get("id") : null; | ||||||||||||||||||||||||||||||||||||||||||
| const initialId = | ||||||||||||||||||||||||||||||||||||||||||
| typeof window !== "undefined" ? new URL(window.location.href).searchParams.get("id") : null; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| async function handleExport(hours: number) { | ||||||||||||||||||||||||||||||||||||||||||
| setExporting(true); | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -64,26 +79,40 @@ export default function LogsPage() { | |||||||||||||||||||||||||||||||||||||||||
| setShowCleanHistory(false); | ||||||||||||||||||||||||||||||||||||||||||
| setCleanHistoryStatus(null); | ||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||
| const res = await fetch("/api/settings/purge-logs", { method: "POST" }); | ||||||||||||||||||||||||||||||||||||||||||
| const res = await fetch("/api/settings/purge-request-history", { method: "POST" }); | ||||||||||||||||||||||||||||||||||||||||||
| const data = await res.json().catch(() => null); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| if (!res.ok) { | ||||||||||||||||||||||||||||||||||||||||||
| throw new Error(data?.error || "Failed to clean log history."); | ||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||
| data?.error || logsText(t, "cleanHistoryFailed", "Failed to clean log history.") | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const deleted = typeof data?.deleted === "number" ? data.deleted : 0; | ||||||||||||||||||||||||||||||||||||||||||
| const deletedArtifacts = typeof data?.deletedArtifacts === "number" ? data.deletedArtifacts : 0; | ||||||||||||||||||||||||||||||||||||||||||
| const deletedArtifacts = | ||||||||||||||||||||||||||||||||||||||||||
| typeof data?.deletedArtifacts === "number" ? data.deletedArtifacts : 0; | ||||||||||||||||||||||||||||||||||||||||||
| const deletedDetailedLogs = | ||||||||||||||||||||||||||||||||||||||||||
| typeof data?.deletedDetailedLogs === "number" ? data.deletedDetailedLogs : 0; | ||||||||||||||||||||||||||||||||||||||||||
| const successFallback = `Cleaned ${deleted} log entr${deleted === 1 ? "y" : "ies"}, ${deletedArtifacts} artifact${ | ||||||||||||||||||||||||||||||||||||||||||
| deletedArtifacts === 1 ? "" : "s" | ||||||||||||||||||||||||||||||||||||||||||
| }, and ${deletedDetailedLogs} legacy detail row${deletedDetailedLogs === 1 ? "" : "s"}.`; | ||||||||||||||||||||||||||||||||||||||||||
| setRequestLogKey((key) => key + 1); | ||||||||||||||||||||||||||||||||||||||||||
| setCleanHistoryStatus( | ||||||||||||||||||||||||||||||||||||||||||
| deleted || deletedArtifacts | ||||||||||||||||||||||||||||||||||||||||||
| ? `Cleaned ${deleted} log entr${deleted === 1 ? "y" : "ies"} and ${deletedArtifacts} artifact${ | ||||||||||||||||||||||||||||||||||||||||||
| deletedArtifacts === 1 ? "" : "s" | ||||||||||||||||||||||||||||||||||||||||||
| }.` | ||||||||||||||||||||||||||||||||||||||||||
| : "No expired log history needed cleanup." | ||||||||||||||||||||||||||||||||||||||||||
| deleted || deletedArtifacts || deletedDetailedLogs | ||||||||||||||||||||||||||||||||||||||||||
| ? logsText(t, "cleanHistorySuccess", successFallback, { | ||||||||||||||||||||||||||||||||||||||||||
| deleted, | ||||||||||||||||||||||||||||||||||||||||||
| deletedArtifacts, | ||||||||||||||||||||||||||||||||||||||||||
| deletedDetailedLogs, | ||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||
| : logsText(t, "cleanHistoryEmpty", "No request log history was found.") | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
100
to
108
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The user-facing status messages for the clean history action are hardcoded in English. Since the project uses
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||
| console.error("Failed to clean log history", err); | ||||||||||||||||||||||||||||||||||||||||||
| setCleanHistoryStatus(err instanceof Error ? err.message : "Failed to clean log history."); | ||||||||||||||||||||||||||||||||||||||||||
| console.error(logsText(t, "cleanHistoryFailed", "Failed to clean log history."), err); | ||||||||||||||||||||||||||||||||||||||||||
| setCleanHistoryStatus( | ||||||||||||||||||||||||||||||||||||||||||
| err instanceof Error | ||||||||||||||||||||||||||||||||||||||||||
| ? err.message | ||||||||||||||||||||||||||||||||||||||||||
| : logsText(t, "cleanHistoryFailed", "Failed to clean log history.") | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||
| setCleaningHistory(false); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -120,7 +149,7 @@ export default function LogsPage() { | |||||||||||||||||||||||||||||||||||||||||
| strokeLinejoin="round" | ||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||
| </svg> | ||||||||||||||||||||||||||||||||||||||||||
| Clean history | ||||||||||||||||||||||||||||||||||||||||||
| {logsText(t, "cleanHistoryButton", "Clean history")} | ||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| <div className="relative" ref={dropdownRef}> | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -195,10 +224,14 @@ export default function LogsPage() { | |||||||||||||||||||||||||||||||||||||||||
| isOpen={showCleanHistory} | ||||||||||||||||||||||||||||||||||||||||||
| onClose={() => setShowCleanHistory(false)} | ||||||||||||||||||||||||||||||||||||||||||
| onConfirm={handleCleanHistory} | ||||||||||||||||||||||||||||||||||||||||||
| title="Clean log history?" | ||||||||||||||||||||||||||||||||||||||||||
| message="This clears expired log history and prunes related artifacts using the current retention policy. The live page will refresh after cleanup." | ||||||||||||||||||||||||||||||||||||||||||
| confirmText="Clean history" | ||||||||||||||||||||||||||||||||||||||||||
| cancelText="Cancel" | ||||||||||||||||||||||||||||||||||||||||||
| title={logsText(t, "cleanHistoryTitle", "Clean log history?")} | ||||||||||||||||||||||||||||||||||||||||||
| message={logsText( | ||||||||||||||||||||||||||||||||||||||||||
| t, | ||||||||||||||||||||||||||||||||||||||||||
| "cleanHistoryMessage", | ||||||||||||||||||||||||||||||||||||||||||
| "This permanently clears all request log rows, legacy detail rows, and local artifact files under DATA_DIR/call_logs. The live page will refresh after cleanup." | ||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||
| confirmText={logsText(t, "cleanHistoryConfirm", "Clean history")} | ||||||||||||||||||||||||||||||||||||||||||
| cancelText={logsText(t, "cleanHistoryCancel", "Cancel")} | ||||||||||||||||||||||||||||||||||||||||||
| loading={cleaningHistory} | ||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { NextResponse } from "next/server"; | ||
| import { buildErrorBody } from "@omniroute/open-sse/utils/error"; | ||
| import { purgeCallLogs, purgeDetailedLogs } from "@/lib/db/cleanup"; | ||
| import { isAuthenticated } from "@/shared/utils/apiAuth"; | ||
|
|
||
| export const runtime = "nodejs"; | ||
|
|
||
| export async function POST(request: Request) { | ||
|
Comment on lines
+1
to
+8
|
||
| if (!(await isAuthenticated(request))) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| try { | ||
| const callLogs = await purgeCallLogs(); | ||
| const detailedLogs = await purgeDetailedLogs(); | ||
| const errors = callLogs.errors + detailedLogs.errors; | ||
|
|
||
| return NextResponse.json( | ||
| { | ||
| deleted: callLogs.deleted, | ||
| deletedArtifacts: callLogs.deletedArtifacts ?? 0, | ||
| deletedDetailedLogs: detailedLogs.deleted, | ||
| errors, | ||
| }, | ||
| { status: errors > 0 ? 500 : 200 } | ||
| ); | ||
| } catch { | ||
| return NextResponse.json(buildErrorBody(500, "Failed to purge request history"), { | ||
| status: 500, | ||
| }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,6 +61,11 @@ export type CallLogArtifactWriteResult = { | |
| sha256: string; | ||
| }; | ||
|
|
||
| export type PurgeCallLogArtifactDirectoryResult = { | ||
| deletedArtifacts: number; | ||
| errors: number; | ||
| }; | ||
|
|
||
| export function buildArtifactRelativePath(timestamp: string, id: string) { | ||
| const parsed = new Date(timestamp); | ||
| const safeTimestamp = ( | ||
|
|
@@ -307,3 +312,26 @@ export function listCallLogArtifactFiles(baseDir = CALL_LOGS_DIR) { | |
| }) | ||
| .sort((a, b) => b.mtimeMs - a.mtimeMs); | ||
| } | ||
|
|
||
| export function purgeCallLogArtifactDirectory( | ||
| baseDir = CALL_LOGS_DIR | ||
| ): PurgeCallLogArtifactDirectoryResult { | ||
| const result = { deletedArtifacts: 0, errors: 0 }; | ||
| if (!baseDir || !fs.existsSync(baseDir)) return result; | ||
|
|
||
| try { | ||
| result.deletedArtifacts = listCallLogArtifactFiles(baseDir).length; | ||
| } catch { | ||
| result.deletedArtifacts = 0; | ||
| } | ||
|
|
||
| try { | ||
| fs.rmSync(baseDir, { recursive: true, force: true }); | ||
| } catch (error) { | ||
| console.error("[callLogArtifacts] Failed to purge call log artifacts:", error); | ||
| result.deletedArtifacts = 0; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: Lost count on deletion failure - deletedArtifacts reset to 0 when fs.rmSync fails, losing the count computed before deletion attempt |
||
| result.errors++; | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CRITICAL: Incomplete 500 response schema - the error object lacks required properties (message, type, code) that buildErrorBody returns. Should reference #/components/schemas/ErrorResponse