feat(db): include xp_audit_log in automatic retention/prune (#6801) (#7260)

* feat(db): include xp_audit_log in automatic retention/prune (#6801)

* fix(i18n): mirror retentionXpAuditLog into pt-BR.json (#6801)

pt.json and pt-BR.json are distinct files; the key landed only in pt.json,
so the i18n pt-BR integrity test (no drift, #6695) went red.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-17 10:41:17 -03:00
committed by GitHub
parent 8b9c7734b8
commit 29bb59e18d
10 changed files with 162 additions and 0 deletions

View File

@@ -0,0 +1 @@
- feat(db): include `xp_audit_log` in the automatic retention/prune cycle, with a configurable `retention.xpAuditLog` setting (#6801)

View File

@@ -910,6 +910,7 @@ export default function SystemStorageTab() {
["callLogs", t("retentionCallLogs"), 30],
["usageHistory", t("retentionUsageHistory"), 30],
["memoryEntries", t("retentionMemoryEntries"), 30],
["xpAuditLog", t("retentionXpAuditLog"), 30],
];
return (

View File

@@ -5967,6 +5967,7 @@
"retentionCallLogs": "Call Logs (days)",
"retentionUsageHistory": "Usage History (days)",
"retentionMemoryEntries": "Memory Entries (days)",
"retentionXpAuditLog": "XP Audit Log (days)",
"saveRetentionSettings": "Save retention settings",
"storageAutoVacuumMode": "Auto Vacuum Mode",
"storageScheduledVacuum": "Scheduled Vacuum",

View File

@@ -5929,6 +5929,7 @@
"retentionCallLogs": "Registros de chamadas (dias)",
"retentionUsageHistory": "Histórico de uso (dias)",
"retentionMemoryEntries": "Entradas de memória (dias)",
"retentionXpAuditLog": "Log de Auditoria de XP (dias)",
"saveRetentionSettings": "__MISSING__:Save retention settings",
"storageAutoVacuumMode": "Modo de vácuo automático",
"storageScheduledVacuum": "Vácuo programado",

View File

@@ -5825,6 +5825,7 @@
"retentionCallLogs": "Registros de chamadas (dias)",
"retentionUsageHistory": "Histórico de uso (dias)",
"retentionMemoryEntries": "Entradas de memória (dias)",
"retentionXpAuditLog": "Log de Auditoria de XP (dias)",
"saveRetentionSettings": "__MISSING__:Save retention settings",
"storageAutoVacuumMode": "Modo de vácuo automático",
"storageScheduledVacuum": "Vácuo programado",

View File

@@ -244,6 +244,36 @@ export async function cleanupMemoryEntries(): Promise<CleanupResult> {
return result;
}
/**
* Clean up old xp_audit_log based on retention settings.
*/
export async function cleanupXpAuditLog(): Promise<CleanupResult> {
const db = getDbInstance();
const retention = getRetentionSettings();
const retentionDays = retention.xpAuditLog;
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
const cutoffISO = cutoffDate.toISOString();
const result: CleanupResult = { deleted: 0, errors: 0 };
try {
const stmt = db.prepare("DELETE FROM xp_audit_log WHERE created_at < ?");
const runResult = stmt.run(cutoffISO);
result.deleted = runResult.changes;
console.log(
`[Cleanup] Deleted ${result.deleted} xp_audit_log older than ${retentionDays} days`
);
} catch (err: unknown) {
console.error("[Cleanup] Error cleaning xp_audit_log:", err);
result.errors++;
}
return result;
}
/**
* Run all cleanup functions if auto-cleanup is enabled.
*/
@@ -270,6 +300,7 @@ export async function runAutoCleanup(): Promise<{
mcpAudit: await cleanupMcpAudit(),
a2aEvents: await cleanupA2aEvents(),
memoryEntries: await cleanupMemoryEntries(),
xpAuditLog: await cleanupXpAuditLog(),
proxyLogs: await cleanupProxyLogs(),
};

View File

@@ -49,6 +49,7 @@ const LEGACY_FLAT_KEYS: {
callLogs: ["callLogs"],
usageHistory: ["usageHistory"],
memoryEntries: ["memoryEntries"],
xpAuditLog: ["xpAuditLog"],
autoCleanupEnabled: ["autoCleanupEnabled"],
},
aggregation: {

View File

@@ -379,6 +379,7 @@ export const databaseSettingsSchema = z
callLogs: z.number().int().min(1).max(3650),
usageHistory: z.number().int().min(1).max(3650),
memoryEntries: z.number().int().min(1).max(3650),
xpAuditLog: z.number().int().min(1).max(365),
autoCleanupEnabled: z.boolean(),
}),

View File

@@ -46,6 +46,7 @@ export interface DatabaseSettings {
callLogs: number;
usageHistory: number;
memoryEntries: number;
xpAuditLog: number;
autoCleanupEnabled: boolean;
};
@@ -106,6 +107,7 @@ export const DEFAULT_DATABASE_SETTINGS: Omit<DatabaseSettings, "location" | "sta
callLogs: 30,
usageHistory: 30,
memoryEntries: 30,
xpAuditLog: 30,
autoCleanupEnabled: true,
},
aggregation: {

View File

@@ -0,0 +1,122 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-xp-audit-cleanup-"));
process.env.DATA_DIR = TEST_DATA_DIR;
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 cleanup = await import("../../src/lib/db/cleanup.ts");
type CountRow = {
count: number;
};
function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
function makeJsonRequest(method: string, body?: unknown): Request {
return new Request("http://localhost/api/settings/database", {
method,
headers: { "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
}
function insertXpAuditLogRow(createdAt: string) {
const db = core.getDbInstance();
db.prepare(
`INSERT INTO xp_audit_log (api_key_id, action, xp_earned, metadata, created_at)
VALUES (?, ?, ?, ?, ?)`
).run("test-api-key", "test-action", 10, null, createdAt);
}
function countXpAuditLogRows(): number {
const db = core.getDbInstance();
const row = db.prepare("SELECT COUNT(*) AS count FROM xp_audit_log").get() as CountRow;
return row.count;
}
test.beforeEach(() => {
resetStorage();
});
test.after(() => {
resetStorage();
});
test("cleanupXpAuditLog deletes rows older than the retention window and keeps recent rows", async () => {
const oldCreatedAt = new Date(Date.now() - 40 * 24 * 60 * 60 * 1000).toISOString();
const recentCreatedAt = new Date().toISOString();
insertXpAuditLogRow(oldCreatedAt);
insertXpAuditLogRow(recentCreatedAt);
const result = await cleanup.cleanupXpAuditLog();
assert.equal(result.errors, 0);
assert.equal(result.deleted, 1);
assert.equal(countXpAuditLogRows(), 1);
});
test("runAutoCleanup includes an xpAuditLog result with numeric deleted/errors fields", async () => {
const oldCreatedAt = new Date(Date.now() - 40 * 24 * 60 * 60 * 1000).toISOString();
insertXpAuditLogRow(oldCreatedAt);
const result = await cleanup.runAutoCleanup();
assert.ok(result.results.xpAuditLog);
assert.equal(typeof result.results.xpAuditLog.deleted, "number");
assert.equal(typeof result.results.xpAuditLog.errors, "number");
assert.equal(result.results.xpAuditLog.deleted, 1);
assert.equal(countXpAuditLogRows(), 0);
});
test("cleanupXpAuditLog honors a configurable retention.xpAuditLog value", async () => {
const tenDaysAgo = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString();
insertXpAuditLogRow(tenDaysAgo);
const current = databaseSettings.getUserDatabaseSettings();
databaseSettings.updateDatabaseSettings({
retention: { ...current.retention, xpAuditLog: 15 },
});
let result = await cleanup.cleanupXpAuditLog();
assert.equal(result.deleted, 0);
assert.equal(countXpAuditLogRows(), 1);
databaseSettings.updateDatabaseSettings({
retention: { ...databaseSettings.getUserDatabaseSettings().retention, xpAuditLog: 5 },
});
result = await cleanup.cleanupXpAuditLog();
assert.equal(result.deleted, 1);
assert.equal(countXpAuditLogRows(), 0);
});
test("PATCH /api/settings/database round-trips retention.xpAuditLog without stripping it", async () => {
const current = databaseSettings.getUserDatabaseSettings();
const response = await databaseSettingsRoute.PATCH(
makeJsonRequest("PATCH", {
retention: { ...current.retention, xpAuditLog: 45 },
}) as never
);
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(body.retention.xpAuditLog, 45);
const getResponse = await databaseSettingsRoute.GET(makeJsonRequest("GET") as never);
const getBody = await getResponse.json();
assert.equal(getResponse.status, 200);
assert.equal(getBody.retention.xpAuditLog, 45);
});