From 29bb59e18d29cd718dfb21f3fc78e43a24b8a8c6 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Fri, 17 Jul 2026 10:41:17 -0300 Subject: [PATCH] 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. --- .../features/6801-xp-audit-log-retention.md | 1 + .../settings/components/SystemStorageTab.tsx | 1 + src/i18n/messages/en.json | 1 + src/i18n/messages/pt-BR.json | 1 + src/i18n/messages/pt.json | 1 + src/lib/db/cleanup.ts | 31 +++++ src/lib/db/databaseSettings.ts | 1 + src/shared/validation/settingsSchemas.ts | 1 + src/types/databaseSettings.ts | 2 + tests/unit/db-cleanup-xp-audit-log.test.ts | 122 ++++++++++++++++++ 10 files changed, 162 insertions(+) create mode 100644 changelog.d/features/6801-xp-audit-log-retention.md create mode 100644 tests/unit/db-cleanup-xp-audit-log.test.ts diff --git a/changelog.d/features/6801-xp-audit-log-retention.md b/changelog.d/features/6801-xp-audit-log-retention.md new file mode 100644 index 0000000000..6847e48cca --- /dev/null +++ b/changelog.d/features/6801-xp-audit-log-retention.md @@ -0,0 +1 @@ +- feat(db): include `xp_audit_log` in the automatic retention/prune cycle, with a configurable `retention.xpAuditLog` setting (#6801) diff --git a/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx b/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx index d73bfe60c0..7f4b57aeac 100644 --- a/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx @@ -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 ( diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index bda8ea1c88..d59d99eed0 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -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", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index c5ab774bae..5f62319c7d 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -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", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index a4aa3942d7..f713c89c30 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -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", diff --git a/src/lib/db/cleanup.ts b/src/lib/db/cleanup.ts index 40cebb328c..5855c140bb 100644 --- a/src/lib/db/cleanup.ts +++ b/src/lib/db/cleanup.ts @@ -244,6 +244,36 @@ export async function cleanupMemoryEntries(): Promise { return result; } +/** + * Clean up old xp_audit_log based on retention settings. + */ +export async function cleanupXpAuditLog(): Promise { + 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(), }; diff --git a/src/lib/db/databaseSettings.ts b/src/lib/db/databaseSettings.ts index fa63534b18..8d037865e3 100644 --- a/src/lib/db/databaseSettings.ts +++ b/src/lib/db/databaseSettings.ts @@ -49,6 +49,7 @@ const LEGACY_FLAT_KEYS: { callLogs: ["callLogs"], usageHistory: ["usageHistory"], memoryEntries: ["memoryEntries"], + xpAuditLog: ["xpAuditLog"], autoCleanupEnabled: ["autoCleanupEnabled"], }, aggregation: { diff --git a/src/shared/validation/settingsSchemas.ts b/src/shared/validation/settingsSchemas.ts index f38034ce07..e3b170925e 100644 --- a/src/shared/validation/settingsSchemas.ts +++ b/src/shared/validation/settingsSchemas.ts @@ -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(), }), diff --git a/src/types/databaseSettings.ts b/src/types/databaseSettings.ts index e07f068244..f69b214111 100644 --- a/src/types/databaseSettings.ts +++ b/src/types/databaseSettings.ts @@ -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 { + 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); +});