From ec78fe3d2c3bd60eb60c946079dcd201d9bbee81 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 13 Jun 2026 00:07:23 -0300 Subject: [PATCH 1/3] fix(publish): clean opencode-plugin node_modules after tsup build to prevent E415 hard-link tarball rejection --- scripts/build/prepublish.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/build/prepublish.ts b/scripts/build/prepublish.ts index b5c2e14dfba2..afcdca2be355 100644 --- a/scripts/build/prepublish.ts +++ b/scripts/build/prepublish.ts @@ -309,6 +309,15 @@ if (existsSync(opencodePluginSrc) && existsSync(join(opencodePluginSrc, "package } else { console.log(" ✅ @omniroute/opencode-plugin dist/ already present (skipping rebuild)"); } + // Remove plugin node_modules after build — hard links created by npm install on Linux + // (CI runner) end up in the tarball as LINK entries, which npm registry rejects with + // E415 "Hard link is not allowed". The node_modules are only needed for the tsup build; + // they must not ship in the published package. + const pluginNodeModules = join(opencodePluginSrc, "node_modules"); + if (existsSync(pluginNodeModules)) { + rmSync(pluginNodeModules, { recursive: true, force: true }); + console.log(" 🧹 Removed @omniroute/opencode-plugin/node_modules (hard link guard)"); + } } else { console.log(" ⏭️ @omniroute/opencode-plugin not found in workspace (skipping build)"); } From 229e1dc1838c1714f3a38ad921204251d50d3112 Mon Sep 17 00:00:00 2001 From: "R.D." Date: Sat, 13 Jun 2026 00:12:47 -0400 Subject: [PATCH 2/3] fix(logs): purge request history artifacts --- docs/getting-started/TROUBLESHOOTING.md | 3 + docs/guides/TROUBLESHOOTING.md | 3 + docs/reference/API_REFERENCE.md | 19 ++--- docs/reference/openapi.yaml | 13 ++++ docs/security/COMPLIANCE.md | 5 ++ src/app/(dashboard)/dashboard/logs/page.tsx | 20 +++--- src/app/api/settings/purge-call-logs/route.ts | 1 + .../settings/purge-request-history/route.ts | 30 ++++++++ src/lib/db/cleanup.ts | 15 ++-- src/lib/usage/callLogArtifacts.ts | 14 ++++ src/lib/usage/callLogs.ts | 20 ++++-- .../database-settings-maintenance.test.ts | 72 +++++++++++++++++++ 12 files changed, 188 insertions(+), 27 deletions(-) create mode 100644 src/app/api/settings/purge-request-history/route.ts diff --git a/docs/getting-started/TROUBLESHOOTING.md b/docs/getting-started/TROUBLESHOOTING.md index 04fcb25f746f..1f79f72b20c4 100644 --- a/docs/getting-started/TROUBLESHOOTING.md +++ b/docs/getting-started/TROUBLESHOOTING.md @@ -259,6 +259,9 @@ curl http://localhost:20128/api/monitoring/health - Application logs: `/logs/...` (when `APP_LOG_TO_FILE=true`) - Call log artifacts: `${DATA_DIR}/call_logs/YYYY-MM-DD/...` when the call log pipeline is enabled +The Request Logs page's **Clean history** action clears `call_logs`, legacy +`request_detail_logs`, and the local `${DATA_DIR}/call_logs/` artifact directory. + --- ## Circuit Breaker Issues diff --git a/docs/guides/TROUBLESHOOTING.md b/docs/guides/TROUBLESHOOTING.md index 01e197fe07e2..fc501b73e445 100644 --- a/docs/guides/TROUBLESHOOTING.md +++ b/docs/guides/TROUBLESHOOTING.md @@ -263,6 +263,9 @@ curl http://localhost:20128/api/monitoring/health - Application logs: `/logs/...` (when `APP_LOG_TO_FILE=true`) - Call log artifacts: `${DATA_DIR}/call_logs/YYYY-MM-DD/...` when the call log pipeline is enabled +The Request Logs page's **Clean history** action clears `call_logs`, legacy +`request_detail_logs`, and the local `${DATA_DIR}/call_logs/` artifact directory. + --- ## Circuit Breaker Issues diff --git a/docs/reference/API_REFERENCE.md b/docs/reference/API_REFERENCE.md index 257f5f204499..2e738f99fb41 100644 --- a/docs/reference/API_REFERENCE.md +++ b/docs/reference/API_REFERENCE.md @@ -404,15 +404,16 @@ Response example: ### Settings -| Endpoint | Method | Description | -| ------------------------------- | ------------- | ------------------------- | -| `/api/settings` | GET/PUT/PATCH | General settings | -| `/api/settings/proxy` | GET/PUT | Network proxy config | -| `/api/settings/proxy/test` | POST | Test proxy connection | -| `/api/settings/ip-filter` | GET/PUT | IP allowlist/blocklist | -| `/api/settings/thinking-budget` | GET/PUT | Reasoning token budget | -| `/api/settings/system-prompt` | GET/PUT | Global system prompt | -| `/api/settings/compression` | GET/PUT | Global compression config | +| Endpoint | Method | Description | +| ------------------------------------- | ------------- | --------------------------------------------------- | +| `/api/settings` | GET/PUT/PATCH | General settings | +| `/api/settings/proxy` | GET/PUT | Network proxy config | +| `/api/settings/proxy/test` | POST | Test proxy connection | +| `/api/settings/ip-filter` | GET/PUT | IP allowlist/blocklist | +| `/api/settings/thinking-budget` | GET/PUT | Reasoning token budget | +| `/api/settings/system-prompt` | GET/PUT | Global system prompt | +| `/api/settings/compression` | GET/PUT | Global compression config | +| `/api/settings/purge-request-history` | POST | Clear request log rows and local call-log artifacts | ### Context & Compression diff --git a/docs/reference/openapi.yaml b/docs/reference/openapi.yaml index 3fce18a61df7..a4e0f6376db5 100644 --- a/docs/reference/openapi.yaml +++ b/docs/reference/openapi.yaml @@ -1803,6 +1803,19 @@ paths: "200": description: Updated settings + /api/settings/purge-request-history: + post: + tags: [Settings] + summary: Clear request log history + description: Deletes `call_logs`, legacy `request_detail_logs`, and local request artifact files under `DATA_DIR/call_logs`. + security: + - ManagementSessionAuth: [] + responses: + "200": + description: Request history cleared + "401": + description: Unauthorized + /api/settings/compression: get: tags: [Compression] diff --git a/docs/security/COMPLIANCE.md b/docs/security/COMPLIANCE.md index c8c4eef8394c..1a3d9f599ee4 100644 --- a/docs/security/COMPLIANCE.md +++ b/docs/security/COMPLIANCE.md @@ -118,6 +118,11 @@ from `src/server-init.ts` and `src/instrumentation-node.ts`. Each run logs a `compliance.cleanup` audit event with the per-table delete counts. Proxy/call log trimming is batched (`BATCH_SIZE = 5000`) to avoid long write locks. +Manual request-history cleanup is separate from retention. The Request Logs +page calls `POST /api/settings/purge-request-history`, which deletes `call_logs`, +legacy `request_detail_logs`, and local request artifacts under +`${DATA_DIR}/call_logs/`. + Defaults are defined in `src/lib/logEnv.ts` (`DEFAULT_APP_LOG_RETENTION_DAYS = 7`, `DEFAULT_CALL_LOG_RETENTION_DAYS = 7`). diff --git a/src/app/(dashboard)/dashboard/logs/page.tsx b/src/app/(dashboard)/dashboard/logs/page.tsx index da13c5a770ff..28fb07a63430 100644 --- a/src/app/(dashboard)/dashboard/logs/page.tsx +++ b/src/app/(dashboard)/dashboard/logs/page.tsx @@ -34,7 +34,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,7 +65,7 @@ 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) { @@ -72,14 +73,17 @@ export default function LogsPage() { } 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; setRequestLogKey((key) => key + 1); setCleanHistoryStatus( - deleted || deletedArtifacts - ? `Cleaned ${deleted} log entr${deleted === 1 ? "y" : "ies"} and ${deletedArtifacts} artifact${ + deleted || deletedArtifacts || deletedDetailedLogs + ? `Cleaned ${deleted} log entr${deleted === 1 ? "y" : "ies"}, ${deletedArtifacts} artifact${ deletedArtifacts === 1 ? "" : "s" - }.` - : "No expired log history needed cleanup." + }, and ${deletedDetailedLogs} legacy detail row${deletedDetailedLogs === 1 ? "" : "s"}.` + : "No request log history was found." ); } catch (err) { console.error("Failed to clean log history", err); @@ -196,7 +200,7 @@ export default function LogsPage() { 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." + message="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="Clean history" cancelText="Cancel" loading={cleaningHistory} diff --git a/src/app/api/settings/purge-call-logs/route.ts b/src/app/api/settings/purge-call-logs/route.ts index 2e5c21d6db72..f17114e08f6b 100644 --- a/src/app/api/settings/purge-call-logs/route.ts +++ b/src/app/api/settings/purge-call-logs/route.ts @@ -10,6 +10,7 @@ export async function POST(request: Request) { const result = await purgeCallLogs(); return NextResponse.json({ deleted: result.deleted, + deletedArtifacts: result.deletedArtifacts ?? 0, errors: result.errors, }); } catch (err: unknown) { diff --git a/src/app/api/settings/purge-request-history/route.ts b/src/app/api/settings/purge-request-history/route.ts new file mode 100644 index 000000000000..8f1f8d52cd2d --- /dev/null +++ b/src/app/api/settings/purge-request-history/route.ts @@ -0,0 +1,30 @@ +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 async function POST(request: Request) { + 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, + }); + } +} diff --git a/src/lib/db/cleanup.ts b/src/lib/db/cleanup.ts index 9d67fb91516c..ec97b2cd147c 100644 --- a/src/lib/db/cleanup.ts +++ b/src/lib/db/cleanup.ts @@ -7,9 +7,11 @@ import { getDbInstance } from "./core"; import { getUserDatabaseSettings } from "./databaseSettings"; import { rollupUsageHistoryBeforeDate } from "@/lib/usage/aggregateHistory"; +import { deleteAllCallLogs } from "@/lib/usage/callLogs"; interface CleanupResult { deleted: number; + deletedArtifacts?: number; errors: number; } @@ -303,15 +305,16 @@ export async function purgeQuotaSnapshots(): Promise { * Purge ALL call_logs immediately (no retention check). */ export async function purgeCallLogs(): Promise { - const db = getDbInstance(); - const result: CleanupResult = { deleted: 0, errors: 0 }; + const result: CleanupResult = { deleted: 0, deletedArtifacts: 0, errors: 0 }; try { - const stmt = db.prepare("DELETE FROM call_logs"); - const runResult = stmt.run(); - result.deleted = runResult.changes; + const runResult = deleteAllCallLogs(); + result.deleted = runResult.deletedRows; + result.deletedArtifacts = runResult.deletedArtifacts; - console.log(`[Cleanup] Purged ${result.deleted} call_logs`); + console.log( + `[Cleanup] Purged ${result.deleted} call_logs and ${result.deletedArtifacts} artifact(s)` + ); } catch (err: unknown) { console.error("[Cleanup] Error purging call_logs:", err); result.errors++; diff --git a/src/lib/usage/callLogArtifacts.ts b/src/lib/usage/callLogArtifacts.ts index 96f8dd454252..f6b6408d7ed1 100644 --- a/src/lib/usage/callLogArtifacts.ts +++ b/src/lib/usage/callLogArtifacts.ts @@ -307,3 +307,17 @@ export function listCallLogArtifactFiles(baseDir = CALL_LOGS_DIR) { }) .sort((a, b) => b.mtimeMs - a.mtimeMs); } + +export function purgeCallLogArtifactDirectory(baseDir = CALL_LOGS_DIR) { + if (!baseDir || !fs.existsSync(baseDir)) return 0; + + let deletedArtifacts = 0; + try { + deletedArtifacts = listCallLogArtifactFiles(baseDir).length; + } catch { + deletedArtifacts = 0; + } + + fs.rmSync(baseDir, { recursive: true, force: true }); + return deletedArtifacts; +} diff --git a/src/lib/usage/callLogs.ts b/src/lib/usage/callLogs.ts index e0096a29ebb7..2c3373b15dbe 100644 --- a/src/lib/usage/callLogs.ts +++ b/src/lib/usage/callLogs.ts @@ -28,6 +28,7 @@ import { cleanupEmptyCallLogDirs, deleteCallArtifact, listCallLogArtifactFiles, + purgeCallLogArtifactDirectory, readCallArtifact, writeCallArtifact, type CallLogArtifact, @@ -506,6 +507,17 @@ export function deleteCallLogsBefore(cutoff: string): DeleteResult { return deleteCallLogRowsByIds(ids); } +export function deleteAllCallLogs(): DeleteResult { + const db = getDbInstance(); + const result = db.prepare("DELETE FROM call_logs").run(); + const deletedArtifacts = purgeCallLogArtifactDirectory(); + + return { + deletedRows: result.changes, + deletedArtifacts, + }; +} + export function trimCallLogsToMaxRows(maxRows = getCallLogsTableMaxRows()) { if (!Number.isInteger(maxRows) || maxRows < 1) { return { deletedRows: 0, deletedArtifacts: 0 }; @@ -899,7 +911,7 @@ export async function getCallLogById(id: string) { error: artifactResult.artifact.error ?? entry.error, pipelinePayloads: artifactResult.artifact.pipeline ?? buildLegacyPipelinePayloads(id), hasPipelineDetails: Boolean(artifactResult.artifact.pipeline) || entry.hasPipelineDetails, - active: false + active: false, }; } @@ -923,7 +935,7 @@ export async function getCallLogById(id: string) { ...legacyInline, pipelinePayloads: legacyPipeline, hasPipelineDetails: Boolean(legacyPipeline) || entry.hasPipelineDetails, - active: false + active: false, }; } } @@ -940,7 +952,7 @@ export async function getCallLogById(id: string) { error: legacyDisk.error ?? entry.error, pipelinePayloads: legacyPipeline, hasPipelineDetails: Boolean(legacyPipeline) || entry.hasPipelineDetails, - active: false + active: false, }; } @@ -954,7 +966,7 @@ export async function getCallLogById(id: string) { error: entry.error, pipelinePayloads: legacyPipeline, hasPipelineDetails: Boolean(legacyPipeline) || entry.hasPipelineDetails, - active: false + active: false, }; } diff --git a/tests/unit/database-settings-maintenance.test.ts b/tests/unit/database-settings-maintenance.test.ts index 449ee5a4eaa5..b29ee88430af 100644 --- a/tests/unit/database-settings-maintenance.test.ts +++ b/tests/unit/database-settings-maintenance.test.ts @@ -11,6 +11,8 @@ process.env.DISABLE_SQLITE_AUTO_BACKUP = "true"; const core = await import("../../src/lib/db/core.ts"); const databaseSettings = await import("../../src/lib/db/databaseSettings.ts"); const databaseSettingsRoute = await import("../../src/app/api/settings/database/route.ts"); +const purgeRequestHistoryRoute = + await import("../../src/app/api/settings/purge-request-history/route.ts"); const settingsDb = await import("../../src/lib/db/settings.ts"); const cleanup = await import("../../src/lib/db/cleanup.ts"); const aggregateHistory = await import("../../src/lib/usage/aggregateHistory.ts"); @@ -40,6 +42,31 @@ function makeJsonRequest(method: string, body?: unknown): Request { }); } +function insertCallLog(id: string, artifactRelPath: string | null = null) { + const db = core.getDbInstance(); + db.prepare( + ` + INSERT INTO call_logs (id, timestamp, method, path, status, detail_state, artifact_relpath) + VALUES (?, ?, ?, ?, ?, ?, ?) + ` + ).run( + id, + "2026-06-01T12:00:00.000Z", + "POST", + "/v1/chat/completions", + 200, + artifactRelPath ? "ready" : "none", + artifactRelPath + ); +} + +function writeCallLogArtifact(relativePath: string) { + const absolutePath = path.join(TEST_DATA_DIR, "call_logs", relativePath); + fs.mkdirSync(path.dirname(absolutePath), { recursive: true }); + fs.writeFileSync(absolutePath, JSON.stringify({ relativePath }), "utf8"); + return absolutePath; +} + test.beforeEach(() => { resetStorage(); }); @@ -142,6 +169,51 @@ test("purgeDetailedLogs deletes request_detail_logs", async () => { ); }); +test("purgeCallLogs deletes summary rows and local request artifacts", async () => { + const db = core.getDbInstance(); + const artifactPath = writeCallLogArtifact("2026-06-01/request-1.json"); + const orphanPath = writeCallLogArtifact("2026-06-02/orphan.json"); + insertCallLog("call-1", "2026-06-01/request-1.json"); + + const result = await cleanup.purgeCallLogs(); + + assert.equal(result.errors, 0); + assert.equal(result.deleted, 1); + assert.equal(result.deletedArtifacts, 2); + assert.equal((db.prepare("SELECT COUNT(*) AS count FROM call_logs").get() as CountRow).count, 0); + assert.equal(fs.existsSync(artifactPath), false); + assert.equal(fs.existsSync(orphanPath), false); + assert.equal(fs.existsSync(path.join(TEST_DATA_DIR, "call_logs")), false); +}); + +test("purge request history route clears call logs, artifacts, and legacy detail rows", async () => { + const db = core.getDbInstance(); + const artifactPath = writeCallLogArtifact("2026-06-01/request-route.json"); + insertCallLog("call-route", "2026-06-01/request-route.json"); + db.prepare("INSERT INTO request_detail_logs (id, timestamp, duration_ms) VALUES (?, ?, ?)").run( + "detail-route", + "2026-06-01T12:00:01.000Z", + 25 + ); + + const response = await purgeRequestHistoryRoute.POST( + new Request("http://localhost/api/settings/purge-request-history", { method: "POST" }) + ); + const body = await response.json(); + + assert.equal(response.status, 200); + assert.equal(body.deleted, 1); + assert.equal(body.deletedArtifacts, 1); + assert.equal(body.deletedDetailedLogs, 1); + assert.equal(body.errors, 0); + assert.equal((db.prepare("SELECT COUNT(*) AS count FROM call_logs").get() as CountRow).count, 0); + assert.equal( + (db.prepare("SELECT COUNT(*) AS count FROM request_detail_logs").get() as CountRow).count, + 0 + ); + assert.equal(fs.existsSync(artifactPath), false); +}); + test("usage aggregation upserts replace recomputed totals instead of adding them twice", async () => { const db = core.getDbInstance(); const insertSnapshot = db.prepare( From 60486107bf6e9f97a3a203b2befc088a65614c4f Mon Sep 17 00:00:00 2001 From: "R.D." Date: Sat, 13 Jun 2026 00:27:08 -0400 Subject: [PATCH 3/3] fix(logs): harden request history purge --- docs/reference/openapi.yaml | 30 ++++++++++ src/app/(dashboard)/dashboard/logs/page.tsx | 55 ++++++++++++++----- src/app/api/settings/purge-call-logs/route.ts | 10 +++- .../settings/purge-request-history/route.ts | 2 + src/i18n/messages/en.json | 8 +++ src/lib/db/cleanup.ts | 20 ++++--- src/lib/usage/callLogArtifacts.ts | 28 +++++++--- src/lib/usage/callLogs.ts | 12 ---- 8 files changed, 123 insertions(+), 42 deletions(-) diff --git a/docs/reference/openapi.yaml b/docs/reference/openapi.yaml index a4e0f6376db5..0e8b88db151c 100644 --- a/docs/reference/openapi.yaml +++ b/docs/reference/openapi.yaml @@ -1813,8 +1813,38 @@ paths: responses: "200": description: Request history cleared + content: + application/json: + schema: + type: object + properties: + deleted: + type: integer + deletedArtifacts: + type: integer + deletedDetailedLogs: + type: integer + errors: + type: integer "401": description: Unauthorized + "500": + description: Cleanup failed or reported errors + content: + application/json: + schema: + type: object + properties: + deleted: + type: integer + deletedArtifacts: + type: integer + deletedDetailedLogs: + type: integer + errors: + type: integer + error: + type: object /api/settings/compression: get: diff --git a/src/app/(dashboard)/dashboard/logs/page.tsx b/src/app/(dashboard)/dashboard/logs/page.tsx index 28fb07a63430..1fd393f83c30 100644 --- a/src/app/(dashboard)/dashboard/logs/page.tsx +++ b/src/app/(dashboard)/dashboard/logs/page.tsx @@ -12,6 +12,20 @@ const TIME_RANGES = [ { label: "24h", hours: 24 }, ]; +type LogsTranslator = ((key: string, values?: Record) => string) & { + has?: (key: string) => boolean; +}; + +function logsText( + t: LogsTranslator, + key: string, + fallback: string, + values?: Record +) { + 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(null); const requestLoggerRef = useRef(null); - const t = useTranslations("logs"); + const t = useTranslations("logs") as LogsTranslator; useEffect(() => { function handleClickOutside(e: MouseEvent) { @@ -69,7 +83,9 @@ export default function LogsPage() { 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; @@ -77,17 +93,26 @@ export default function LogsPage() { 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 || deletedDetailedLogs - ? `Cleaned ${deleted} log entr${deleted === 1 ? "y" : "ies"}, ${deletedArtifacts} artifact${ - deletedArtifacts === 1 ? "" : "s" - }, and ${deletedDetailedLogs} legacy detail row${deletedDetailedLogs === 1 ? "" : "s"}.` - : "No request log history was found." + ? logsText(t, "cleanHistorySuccess", successFallback, { + deleted, + deletedArtifacts, + deletedDetailedLogs, + }) + : logsText(t, "cleanHistoryEmpty", "No request log history was found.") ); } 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); } @@ -124,7 +149,7 @@ export default function LogsPage() { strokeLinejoin="round" /> - Clean history + {logsText(t, "cleanHistoryButton", "Clean history")}
@@ -199,10 +224,14 @@ export default function LogsPage() { isOpen={showCleanHistory} onClose={() => setShowCleanHistory(false)} onConfirm={handleCleanHistory} - title="Clean log history?" - message="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="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} />
diff --git a/src/app/api/settings/purge-call-logs/route.ts b/src/app/api/settings/purge-call-logs/route.ts index f17114e08f6b..96e1842fd46a 100644 --- a/src/app/api/settings/purge-call-logs/route.ts +++ b/src/app/api/settings/purge-call-logs/route.ts @@ -1,7 +1,10 @@ import { NextResponse } from "next/server"; +import { buildErrorBody } from "@omniroute/open-sse/utils/error"; import { purgeCallLogs } from "@/lib/db/cleanup"; import { isAuthenticated } from "@/shared/utils/apiAuth"; +export const runtime = "nodejs"; + export async function POST(request: Request) { if (!(await isAuthenticated(request))) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); @@ -13,8 +16,9 @@ export async function POST(request: Request) { deletedArtifacts: result.deletedArtifacts ?? 0, errors: result.errors, }); - } catch (err: unknown) { - const error = err instanceof Error ? err.message : String(err); - return NextResponse.json({ error }, { status: 500 }); + } catch { + return NextResponse.json(buildErrorBody(500, "Failed to purge call logs"), { + status: 500, + }); } } diff --git a/src/app/api/settings/purge-request-history/route.ts b/src/app/api/settings/purge-request-history/route.ts index 8f1f8d52cd2d..668280605729 100644 --- a/src/app/api/settings/purge-request-history/route.ts +++ b/src/app/api/settings/purge-request-history/route.ts @@ -3,6 +3,8 @@ 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) { if (!(await isAuthenticated(request))) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 2de9742fc4e8..2baa47296dd9 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -3770,6 +3770,14 @@ "export": "Export", "exporting": "Exporting...", "exportFailed": "Export failed", + "cleanHistoryButton": "Clean history", + "cleanHistoryTitle": "Clean log history?", + "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.", + "cleanHistoryConfirm": "Clean history", + "cleanHistoryCancel": "Cancel", + "cleanHistorySuccess": "Cleaned {deleted} log entries, {deletedArtifacts} artifacts, and {deletedDetailedLogs} legacy detail rows.", + "cleanHistoryEmpty": "No request log history was found.", + "cleanHistoryFailed": "Failed to clean log history.", "timeRange": "Time Range", "lastNHours": "Last {hours}", "defaultRange": "default", diff --git a/src/lib/db/cleanup.ts b/src/lib/db/cleanup.ts index ec97b2cd147c..1772240fdf57 100644 --- a/src/lib/db/cleanup.ts +++ b/src/lib/db/cleanup.ts @@ -7,7 +7,7 @@ import { getDbInstance } from "./core"; import { getUserDatabaseSettings } from "./databaseSettings"; import { rollupUsageHistoryBeforeDate } from "@/lib/usage/aggregateHistory"; -import { deleteAllCallLogs } from "@/lib/usage/callLogs"; +import { purgeCallLogArtifactDirectory } from "@/lib/usage/callLogArtifacts"; interface CleanupResult { deleted: number; @@ -305,21 +305,27 @@ export async function purgeQuotaSnapshots(): Promise { * Purge ALL call_logs immediately (no retention check). */ export async function purgeCallLogs(): Promise { + const db = getDbInstance(); const result: CleanupResult = { deleted: 0, deletedArtifacts: 0, errors: 0 }; try { - const runResult = deleteAllCallLogs(); - result.deleted = runResult.deletedRows; - result.deletedArtifacts = runResult.deletedArtifacts; + const runResult = db.prepare("DELETE FROM call_logs").run(); + result.deleted = runResult.changes; - console.log( - `[Cleanup] Purged ${result.deleted} call_logs and ${result.deletedArtifacts} artifact(s)` - ); + console.log(`[Cleanup] Purged ${result.deleted} call_logs`); } catch (err: unknown) { console.error("[Cleanup] Error purging call_logs:", err); result.errors++; } + const artifactResult = purgeCallLogArtifactDirectory(); + result.deletedArtifacts = artifactResult.deletedArtifacts; + result.errors += artifactResult.errors; + + if (artifactResult.errors === 0) { + console.log(`[Cleanup] Purged ${result.deletedArtifacts} call log artifact(s)`); + } + return result; } diff --git a/src/lib/usage/callLogArtifacts.ts b/src/lib/usage/callLogArtifacts.ts index f6b6408d7ed1..e1e1913571f8 100644 --- a/src/lib/usage/callLogArtifacts.ts +++ b/src/lib/usage/callLogArtifacts.ts @@ -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 = ( @@ -308,16 +313,25 @@ export function listCallLogArtifactFiles(baseDir = CALL_LOGS_DIR) { .sort((a, b) => b.mtimeMs - a.mtimeMs); } -export function purgeCallLogArtifactDirectory(baseDir = CALL_LOGS_DIR) { - if (!baseDir || !fs.existsSync(baseDir)) return 0; +export function purgeCallLogArtifactDirectory( + baseDir = CALL_LOGS_DIR +): PurgeCallLogArtifactDirectoryResult { + const result = { deletedArtifacts: 0, errors: 0 }; + if (!baseDir || !fs.existsSync(baseDir)) return result; - let deletedArtifacts = 0; try { - deletedArtifacts = listCallLogArtifactFiles(baseDir).length; + result.deletedArtifacts = listCallLogArtifactFiles(baseDir).length; } catch { - deletedArtifacts = 0; + 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; + result.errors++; } - fs.rmSync(baseDir, { recursive: true, force: true }); - return deletedArtifacts; + return result; } diff --git a/src/lib/usage/callLogs.ts b/src/lib/usage/callLogs.ts index 2c3373b15dbe..e18bbf140a19 100644 --- a/src/lib/usage/callLogs.ts +++ b/src/lib/usage/callLogs.ts @@ -28,7 +28,6 @@ import { cleanupEmptyCallLogDirs, deleteCallArtifact, listCallLogArtifactFiles, - purgeCallLogArtifactDirectory, readCallArtifact, writeCallArtifact, type CallLogArtifact, @@ -507,17 +506,6 @@ export function deleteCallLogsBefore(cutoff: string): DeleteResult { return deleteCallLogRowsByIds(ids); } -export function deleteAllCallLogs(): DeleteResult { - const db = getDbInstance(); - const result = db.prepare("DELETE FROM call_logs").run(); - const deletedArtifacts = purgeCallLogArtifactDirectory(); - - return { - deletedRows: result.changes, - deletedArtifacts, - }; -} - export function trimCallLogsToMaxRows(maxRows = getCallLogsTableMaxRows()) { if (!Number.isInteger(maxRows) || maxRows < 1) { return { deletedRows: 0, deletedArtifacts: 0 };