mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 21:32:10 +03:00
* 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:
committed by
GitHub
parent
8b9c7734b8
commit
29bb59e18d
1
changelog.d/features/6801-xp-audit-log-retention.md
Normal file
1
changelog.d/features/6801-xp-audit-log-retention.md
Normal file
@@ -0,0 +1 @@
|
||||
- feat(db): include `xp_audit_log` in the automatic retention/prune cycle, with a configurable `retention.xpAuditLog` setting (#6801)
|
||||
@@ -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 (
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ const LEGACY_FLAT_KEYS: {
|
||||
callLogs: ["callLogs"],
|
||||
usageHistory: ["usageHistory"],
|
||||
memoryEntries: ["memoryEntries"],
|
||||
xpAuditLog: ["xpAuditLog"],
|
||||
autoCleanupEnabled: ["autoCleanupEnabled"],
|
||||
},
|
||||
aggregation: {
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
122
tests/unit/db-cleanup-xp-audit-log.test.ts
Normal file
122
tests/unit/db-cleanup-xp-audit-log.test.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user