Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/getting-started/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ curl http://localhost:20128/api/monitoring/health
- Application logs: `<repo>/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
Expand Down
3 changes: 3 additions & 0 deletions docs/guides/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ curl http://localhost:20128/api/monitoring/health
- Application logs: `<repo>/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
Expand Down
19 changes: 10 additions & 9 deletions docs/reference/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
43 changes: 43 additions & 0 deletions docs/reference/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1803,6 +1803,49 @@ 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
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:

Copy link
Copy Markdown

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

type: object

Comment on lines +1806 to +1848
/api/settings/compression:
get:
tags: [Compression]
Expand Down
5 changes: 5 additions & 0 deletions docs/security/COMPLIANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).

Expand Down
67 changes: 50 additions & 17 deletions src/app/(dashboard)/dashboard/logs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The user-facing status messages for the clean history action are hardcoded in English. Since the project uses next-intl for internationalization (as seen by useTranslations("logs")), these strings should be localized using the translation helper t(...) with proper pluralization support to adhere to the project's i18n standards.

Suggested change
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."
);
setCleanHistoryStatus(
deleted || deletedArtifacts || deletedDetailedLogs
? t("cleanHistorySuccess", {
deleted,
deletedArtifacts,
deletedDetailedLogs
})
: t("noHistoryFound")
);

} 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);
}
Expand Down Expand Up @@ -120,7 +149,7 @@ export default function LogsPage() {
strokeLinejoin="round"
/>
</svg>
Clean history
{logsText(t, "cleanHistoryButton", "Clean history")}
</button>

<div className="relative" ref={dropdownRef}>
Expand Down Expand Up @@ -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>
Expand Down
11 changes: 8 additions & 3 deletions src/app/api/settings/purge-call-logs/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
Expand All @@ -10,10 +13,12 @@ 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) {
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,
});
}
}
32 changes: 32 additions & 0 deletions src/app/api/settings/purge-request-history/route.ts
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,
});
}
}
8 changes: 8 additions & 0 deletions src/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3771,6 +3771,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",
Expand Down
15 changes: 12 additions & 3 deletions src/lib/db/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
import { getDbInstance } from "./core";
import { getUserDatabaseSettings } from "./databaseSettings";
import { rollupUsageHistoryBeforeDate } from "@/lib/usage/aggregateHistory";
import { purgeCallLogArtifactDirectory } from "@/lib/usage/callLogArtifacts";

interface CleanupResult {
deleted: number;
deletedArtifacts?: number;
errors: number;
}

Expand Down Expand Up @@ -304,11 +306,10 @@ export async function purgeQuotaSnapshots(): Promise<CleanupResult> {
*/
export async function purgeCallLogs(): Promise<CleanupResult> {
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();
const runResult = db.prepare("DELETE FROM call_logs").run();
result.deleted = runResult.changes;

console.log(`[Cleanup] Purged ${result.deleted} call_logs`);
Expand All @@ -317,6 +318,14 @@ export async function purgeCallLogs(): Promise<CleanupResult> {
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;
}

Expand Down
28 changes: 28 additions & 0 deletions src/lib/usage/callLogArtifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down Expand Up @@ -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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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;
}
Loading