fix(usage): reset logs and show provider names in analytics (#7264)

* fix(usage): reset logs and show provider names in analytics

* refactor(db): extract usage purge routines to cleanup module (file-size cap)

Move the generic delete-all/delete-before-cutoff table helpers and the
call-log-artifact purge helpers out of cleanup.ts into a new
cleanup/usagePurge.ts submodule, so this PR's growth in cleanup.ts stays
under the file-size gate cap once combined with other in-flight changes
to the same file. Pure extraction — resetUsageHistory delegates to the
same logic, now imported instead of inlined; no behavior change.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(db): hoist reset targets table + derive total via reduce (max-lines-per-function)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(api): move analytics provider-name enrichment into lib (file-size cap)

src/app/api/usage/analytics/route.ts is a frozen file-size-capped file
(baseline: 942 lines, zero headroom on this branch). The provider
display-name enrichment added for the byProvider breakdown (id -> name/
prefix lookup via provider_nodes) pushed it to 971 lines, tripping the
check:file-size ratchet.

Move the new getProviderDisplayName/getProviderDisplayNames helpers, plus
the byProvider row-building they modified, into a new leaf module
src/lib/usage/providerDisplayNames.ts (buildByProviderRows). The route now
just imports and calls it. No behavior change: same lookup, same fallback
to the raw provider id, same row shape.

Net effect: route.ts drops from 941 (pre-change) to 930 lines (-11),
comfortably restoring headroom instead of exceeding the frozen cap.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(usage): fall back to static catalog name in provider display resolution

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
SeaXen
2026-07-19 06:18:18 +06:00
committed by GitHub
parent c8a9bad000
commit e092280718
8 changed files with 373 additions and 51 deletions

View File

@@ -1541,7 +1541,7 @@ export default function SystemStorageTab() {
<div className="space-y-3">
<p className="text-text-muted">
{t("resetUsageDataDesc") ||
"Select how far back you want to delete usage data. This action cannot be undone."}
"Select how far back you want to delete usage, request logs, and analytics data. Provider configuration, connections, API keys, combos, and settings are preserved. This action cannot be undone."}
</p>
<select
value={resetUsagePeriod}

View File

@@ -44,6 +44,16 @@ export async function POST(request: Request) {
deletedUsageHistory: result.deletedUsageHistory,
deletedDailySummary: result.deletedDailySummary,
deletedHourlySummary: result.deletedHourlySummary,
deletedCallLogs: result.deletedCallLogs,
deletedCallLogArtifacts: result.deletedCallLogArtifacts,
deletedRequestDetailLogs: result.deletedRequestDetailLogs,
deletedProxyLogs: result.deletedProxyLogs,
deletedRelayLogs: result.deletedRelayLogs,
deletedCompressionAnalytics: result.deletedCompressionAnalytics,
deletedCompressionRunTelemetry: result.deletedCompressionRunTelemetry,
deletedRoutingDecisions: result.deletedRoutingDecisions,
deletedQuotaConsumption: result.deletedQuotaConsumption,
deletedTokenLedger: result.deletedTokenLedger,
errors: result.errors,
},
{ status: result.errors > 0 ? 500 : 200 }

View File

@@ -22,6 +22,7 @@ import {
getPresetCostModelRows,
} from "@/lib/db/usageAnalytics";
import { getFallbackStats } from "@/lib/db/callLogStats";
import { buildByProviderRows } from "@/lib/usage/providerDisplayNames";
function getRangeStartIso(range: string): string | null {
const end = new Date();
@@ -668,19 +669,7 @@ export async function GET(request: Request) {
providerCostByProvider.set(provider, (providerCostByProvider.get(provider) || 0) + cost);
}
const byProvider = providerRows.map((row) => ({
provider: getProviderById(toStringValue(row.provider))?.name ?? toStringValue(row.provider),
requests: Number(row.requests),
promptTokens: Number(row.promptTokens),
completionTokens: Number(row.completionTokens),
totalTokens: Number(row.totalTokens),
avgLatencyMs: Math.round(Number(row.avgLatencyMs)),
successRatePct:
Number(row.requests) > 0
? Number((Number(row.successfulRequests) / Number(row.requests)) * 100).toFixed(2)
: 0,
cost: roundCost(providerCostByProvider.get(toStringValue(row.provider)) || 0),
}));
const byProvider = await buildByProviderRows(providerRows, providerCostByProvider);
const accountCostByAccount = new Map<string, number>();
for (const row of accountCostRows) {

View File

@@ -5686,7 +5686,7 @@
"purgeLogsFailed": "Failed to purge logs",
"logsDeleted": "{count, plural, =0 {No expired logs purged} one {Purged # expired log} other {Purged # expired logs}}",
"resetUsageData": "Reset Usage Data",
"resetUsageDataDesc": "Select how far back you want to delete usage data. This action cannot be undone.",
"resetUsageDataDesc": "Select how far back you want to delete usage, request logs, and analytics data. Provider configuration, connections, API keys, combos, and settings are preserved. This action cannot be undone.",
"resetUsagePeriod_5m": "5 minutes",
"resetUsagePeriod_1h": "1 hour",
"resetUsagePeriod_3h": "3 hours",

View File

@@ -8,6 +8,13 @@ import { getDbInstance } from "./core";
import { getUserDatabaseSettings } from "./databaseSettings";
import { rollupUsageHistoryBeforeDate } from "@/lib/usage/aggregateHistory";
import { purgeCallLogArtifactDirectory } from "@/lib/usage/callLogArtifacts";
import {
collectCallLogArtifactsBefore,
deleteAllFromTable,
deleteCallLogArtifacts,
deleteFromTableBefore,
type DeleteByPeriodTarget,
} from "./cleanup/usagePurge";
interface CleanupResult {
deleted: number;
@@ -509,6 +516,16 @@ export interface ResetUsageHistoryResult extends CleanupResult {
deletedUsageHistory: number;
deletedDailySummary: number;
deletedHourlySummary: number;
deletedCallLogs: number;
deletedCallLogArtifacts: number;
deletedRequestDetailLogs: number;
deletedProxyLogs: number;
deletedRelayLogs: number;
deletedCompressionAnalytics: number;
deletedCompressionRunTelemetry: number;
deletedRoutingDecisions: number;
deletedQuotaConsumption: number;
deletedTokenLedger: number;
}
function isResetUsageHistoryPeriod(period: string): period is ResetUsageHistoryPeriod {
@@ -528,6 +545,21 @@ function isResetUsageHistoryPeriod(period: string): period is ResetUsageHistoryP
* every row in all three tables; any other value deletes rows strictly
* older than `now - period`. Throws on an invalid period.
*/
const RESET_TARGETS: Array<DeleteByPeriodTarget & { resultKey: keyof ResetUsageHistoryResult }> = [
{ table: "usage_history", column: "timestamp", cutoff: "iso", resultKey: "deletedUsageHistory" },
{ table: "daily_usage_summary", column: "date", cutoff: "date", resultKey: "deletedDailySummary" },
{ table: "hourly_usage_summary", column: "date_hour", cutoff: "dateHour", resultKey: "deletedHourlySummary" },
{ table: "call_logs", column: "timestamp", cutoff: "iso", resultKey: "deletedCallLogs" },
{ table: "request_detail_logs", column: "timestamp", cutoff: "iso", resultKey: "deletedRequestDetailLogs" },
{ table: "proxy_logs", column: "timestamp", cutoff: "iso", resultKey: "deletedProxyLogs" },
{ table: "relay_logs", column: "created_at", cutoff: "epochSeconds", resultKey: "deletedRelayLogs" },
{ table: "compression_analytics", column: "timestamp", cutoff: "iso", resultKey: "deletedCompressionAnalytics" },
{ table: "compression_run_telemetry", column: "timestamp", cutoff: "epochMs", resultKey: "deletedCompressionRunTelemetry" },
{ table: "routing_decisions", column: "created_at", cutoff: "iso", resultKey: "deletedRoutingDecisions" },
{ table: "quota_consumption", column: "updated_at", cutoff: "epochMs", resultKey: "deletedQuotaConsumption" },
{ table: "token_ledger", column: "created_at", cutoff: "iso", resultKey: "deletedTokenLedger" },
];
export async function resetUsageHistory(period: string): Promise<ResetUsageHistoryResult> {
if (!isResetUsageHistoryPeriod(period)) {
throw new Error(`Invalid reset period: ${period}`);
@@ -539,53 +571,55 @@ export async function resetUsageHistory(period: string): Promise<ResetUsageHisto
deletedUsageHistory: 0,
deletedDailySummary: 0,
deletedHourlySummary: 0,
deletedCallLogs: 0,
deletedCallLogArtifacts: 0,
deletedRequestDetailLogs: 0,
deletedProxyLogs: 0,
deletedRelayLogs: 0,
deletedCompressionAnalytics: 0,
deletedCompressionRunTelemetry: 0,
deletedRoutingDecisions: 0,
deletedQuotaConsumption: 0,
deletedTokenLedger: 0,
deletedArtifacts: 0,
errors: 0,
};
try {
let artifactsToDelete: string[] = [];
const runReset = db.transaction(() => {
if (period === "all") {
const usageHistory = db.prepare("DELETE FROM usage_history").run();
const dailySummary = db.prepare("DELETE FROM daily_usage_summary").run();
const hourlySummary = db.prepare("DELETE FROM hourly_usage_summary").run();
result.deletedUsageHistory = usageHistory.changes;
result.deletedDailySummary = dailySummary.changes;
result.deletedHourlySummary = hourlySummary.changes;
for (const target of RESET_TARGETS) {
(result[target.resultKey] as number) = deleteAllFromTable(target.table);
}
return;
}
const cutoffMs = Date.now() - RESET_USAGE_HISTORY_PERIOD_MS[period];
const cutoffIso = new Date(cutoffMs).toISOString();
// usage_history.timestamp is a full ISO string; daily_usage_summary.date is
// "YYYY-MM-DD"; hourly_usage_summary.date_hour is "YYYY-MM-DD HH:00:00" (see
// src/lib/usage/aggregateHistory.ts, which derives both with SQLite's UTC-based
// DATE()/strftime()). Slicing the UTC ISO cutoff keeps all three comparisons
// consistent without re-deriving timezone-sensitive date math by hand.
const cutoffDate = cutoffIso.slice(0, 10);
const cutoffDateHour = `${cutoffIso.slice(0, 10)} ${cutoffIso.slice(11, 13)}:00:00`;
const usageHistory = db
.prepare("DELETE FROM usage_history WHERE timestamp < ?")
.run(cutoffIso);
const dailySummary = db
.prepare("DELETE FROM daily_usage_summary WHERE date < ?")
.run(cutoffDate);
const hourlySummary = db
.prepare("DELETE FROM hourly_usage_summary WHERE date_hour < ?")
.run(cutoffDateHour);
result.deletedUsageHistory = usageHistory.changes;
result.deletedDailySummary = dailySummary.changes;
result.deletedHourlySummary = hourlySummary.changes;
const cutoffIso = new Date(Date.now() - RESET_USAGE_HISTORY_PERIOD_MS[period]).toISOString();
artifactsToDelete = collectCallLogArtifactsBefore(cutoffIso);
for (const target of RESET_TARGETS) {
(result[target.resultKey] as number) = deleteFromTableBefore(target, cutoffIso);
}
});
runReset();
result.deleted =
result.deletedUsageHistory + result.deletedDailySummary + result.deletedHourlySummary;
let artifactResult: { deletedArtifacts: number; errors: number };
if (period === "all") {
artifactResult = purgeCallLogArtifactDirectory();
} else {
artifactResult = deleteCallLogArtifacts(artifactsToDelete);
}
result.deletedCallLogArtifacts = artifactResult.deletedArtifacts;
result.deletedArtifacts = artifactResult.deletedArtifacts;
result.errors += artifactResult.errors;
result.deleted = RESET_TARGETS.reduce((sum, t) => sum + (result[t.resultKey] as number), 0);
console.log(
`[Cleanup] Reset usage data (period=${period}): ${result.deletedUsageHistory} usage_history, ` +
`${result.deletedDailySummary} daily_usage_summary, ${result.deletedHourlySummary} hourly_usage_summary`
`[Cleanup] Reset usage/log data (period=${period}): ${result.deleted} row(s), ` +
`${result.deletedCallLogArtifacts} call log artifact(s)`
);
} catch (err: unknown) {
console.error("[Cleanup] Error resetting usage history:", err);

View File

@@ -0,0 +1,83 @@
/**
* Low-level table/artifact purge primitives backing {@link resetUsageHistory}
* (see `../cleanup.ts`). Split out of `cleanup.ts` to keep that file under the
* repo's file-size cap — these helpers have no state of their own beyond the
* shared DB singleton, so they are safe to call from any reset routine that
* needs generic "wipe" / "wipe before cutoff" semantics.
*
* @module lib/db/cleanup/usagePurge
*/
import { getDbInstance } from "../core";
import { cleanupEmptyCallLogDirs, deleteCallArtifact } from "@/lib/usage/callLogArtifacts";
export type DeleteByPeriodTarget = {
table: string;
column: string;
cutoff: "iso" | "date" | "dateHour" | "epochMs" | "epochSeconds";
};
export function tableExists(table: string): boolean {
const row = getDbInstance()
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
.get(table) as { name?: string } | undefined;
return Boolean(row?.name);
}
export function deleteAllFromTable(table: string): number {
if (!tableExists(table)) return 0;
return getDbInstance().prepare(`DELETE FROM ${table}`).run().changes;
}
export function deleteFromTableBefore(target: DeleteByPeriodTarget, cutoffIso: string): number {
if (!tableExists(target.table)) return 0;
const cutoff = (() => {
switch (target.cutoff) {
case "date":
return cutoffIso.slice(0, 10);
case "dateHour":
return `${cutoffIso.slice(0, 10)} ${cutoffIso.slice(11, 13)}:00:00`;
case "epochMs":
return new Date(cutoffIso).getTime();
case "epochSeconds":
return Math.floor(new Date(cutoffIso).getTime() / 1000);
case "iso":
default:
return cutoffIso;
}
})();
return getDbInstance()
.prepare(`DELETE FROM ${target.table} WHERE ${target.column} < ?`)
.run(cutoff).changes;
}
export function collectCallLogArtifactsBefore(cutoffIso: string): string[] {
if (!tableExists("call_logs")) return [];
const rows = getDbInstance()
.prepare(
"SELECT artifact_relpath FROM call_logs WHERE timestamp < ? AND artifact_relpath IS NOT NULL"
)
.all(cutoffIso) as Array<{ artifact_relpath?: string | null }>;
return rows
.map((row) => row.artifact_relpath)
.filter((relPath): relPath is string => typeof relPath === "string" && relPath.length > 0);
}
export function deleteCallLogArtifacts(relativePaths: string[]): {
deletedArtifacts: number;
errors: number;
} {
const result = { deletedArtifacts: 0, errors: 0 };
for (const relPath of new Set(relativePaths)) {
if (deleteCallArtifact(relPath)) {
result.deletedArtifacts++;
}
}
cleanupEmptyCallLogDirs();
return result;
}

View File

@@ -0,0 +1,88 @@
/**
* Provider display-name resolution for the usage analytics `byProvider`
* breakdown (`src/app/api/usage/analytics/route.ts`).
*
* Raw `usage_history.provider` values are internal provider ids (e.g. a
* dynamic compatible-provider uuid-suffixed id). This module maps those ids
* to the friendly `name`/`prefix` configured on the matching `provider_nodes`
* row, falling back to the raw id when no node matches, so analytics rows
* show a readable label instead of an internal id.
*
* @module lib/usage/providerDisplayNames
*/
import { getProviderNodes } from "@/models";
import { getProviderById } from "@/shared/constants/providers";
function toStringValue(value: unknown, fallback = ""): string {
return typeof value === "string" && value.trim().length > 0 ? value : fallback;
}
function roundCost(value: number): number {
return Math.round(value * 1_000_000) / 1_000_000;
}
function getProviderDisplayName(
provider: unknown,
providerDisplayNames: Map<string, string>
): string {
const rawProvider = toStringValue(provider, "unknown");
// Configured node name wins; static catalog covers built-ins (e.g. codex →
// "OpenAI Codex") the nodes table doesn't know about; raw id is the last resort.
return (
providerDisplayNames.get(rawProvider) || getProviderById(rawProvider)?.name || rawProvider
);
}
async function getProviderDisplayNames(): Promise<Map<string, string>> {
const displayNames = new Map<string, string>();
const providerNodes = (await getProviderNodes()) as Array<{
id?: unknown;
name?: unknown;
prefix?: unknown;
}>;
for (const node of providerNodes) {
const id = toStringValue(node.id);
if (!id) continue;
const displayName = toStringValue(node.name) || toStringValue(node.prefix) || id;
displayNames.set(id, displayName);
}
return displayNames;
}
export interface ByProviderRow {
provider: string;
requests: number;
promptTokens: number;
completionTokens: number;
totalTokens: number;
avgLatencyMs: number;
successRatePct: number | string;
cost: number;
}
/**
* Builds the `byProvider` analytics rows, resolving each row's raw provider
* id to its configured display name.
*/
export async function buildByProviderRows(
providerRows: Array<Record<string, unknown>>,
providerCostByProvider: Map<string, number>
): Promise<ByProviderRow[]> {
const providerDisplayNames = await getProviderDisplayNames();
return providerRows.map((row) => ({
provider: getProviderDisplayName(row.provider, providerDisplayNames),
requests: Number(row.requests),
promptTokens: Number(row.promptTokens),
completionTokens: Number(row.completionTokens),
totalTokens: Number(row.totalTokens),
avgLatencyMs: Math.round(Number(row.avgLatencyMs)),
successRatePct:
Number(row.requests) > 0
? Number((Number(row.successfulRequests) / Number(row.requests)) * 100).toFixed(2)
: 0,
cost: roundCost(providerCostByProvider.get(toStringValue(row.provider)) || 0),
}));
}

View File

@@ -40,8 +40,10 @@ function teardown() {
}
}
function countRows(db: import("better-sqlite3").Database, table: string): number {
const row = db.prepare(`SELECT COUNT(*) as c FROM ${table}`).get() as { c: number };
function countRows(db: unknown, table: string): number {
const row = (db as { prepare: (sql: string) => { get: () => unknown } })
.prepare(`SELECT COUNT(*) as c FROM ${table}`)
.get() as { c: number };
return row.c;
}
@@ -71,8 +73,33 @@ test("resetUsageHistory: 'all' wipes usage_history, daily_usage_summary, and hou
const recentDate = recentIso.slice(0, 10);
const oldDateHour = `${oldIso.slice(0, 10)} ${oldIso.slice(11, 13)}:00:00`;
const recentDateHour = `${recentIso.slice(0, 10)} ${recentIso.slice(11, 13)}:00:00`;
const oldArtifactRelpath = "2026-01-01/old-call.json";
const recentArtifactRelpath = "2026-01-01/recent-call.json";
const oldArtifactPath = path.join(tempDir, "call_logs", oldArtifactRelpath);
const recentArtifactPath = path.join(tempDir, "call_logs", recentArtifactRelpath);
fs.mkdirSync(path.dirname(oldArtifactPath), { recursive: true });
fs.writeFileSync(oldArtifactPath, "{}");
fs.writeFileSync(recentArtifactPath, "{}");
function seed() {
db.prepare(
"INSERT INTO provider_nodes (id, type, name, prefix, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
).run("openai-compatible-chat-test", "chat", "Custom Test", "custom-test", recentIso, recentIso);
db.prepare("INSERT INTO api_keys (id, name, key, created_at) VALUES (?, ?, ?, ?)").run(
"key-test",
"Test Key",
"sk-test",
recentIso
);
db.prepare("INSERT INTO combos (id, name, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)").run(
"combo-test",
"Test Combo",
"{}",
recentIso,
recentIso
);
db.prepare(
"INSERT INTO usage_history (provider, model, timestamp) VALUES (?, ?, ?)"
).run("openai", "gpt-test", oldIso);
@@ -80,6 +107,33 @@ test("resetUsageHistory: 'all' wipes usage_history, daily_usage_summary, and hou
"INSERT INTO usage_history (provider, model, timestamp) VALUES (?, ?, ?)"
).run("openai", "gpt-test", recentIso);
db.prepare("INSERT INTO call_logs (id, timestamp, artifact_relpath) VALUES (?, ?, ?)").run(
"old-call",
oldIso,
oldArtifactRelpath
);
db.prepare("INSERT INTO call_logs (id, timestamp, artifact_relpath) VALUES (?, ?, ?)").run(
"recent-call",
recentIso,
recentArtifactRelpath
);
db.prepare("INSERT INTO request_detail_logs (id, timestamp) VALUES (?, ?)").run(
"old-detail",
oldIso
);
db.prepare("INSERT INTO request_detail_logs (id, timestamp) VALUES (?, ?)").run(
"recent-detail",
recentIso
);
db.prepare("INSERT INTO proxy_logs (id, timestamp) VALUES (?, ?)").run("old-proxy", oldIso);
db.prepare("INSERT INTO proxy_logs (id, timestamp) VALUES (?, ?)").run("recent-proxy", recentIso);
db.prepare(
"INSERT INTO compression_analytics (timestamp, mode, original_tokens, compressed_tokens, tokens_saved) VALUES (?, ?, ?, ?, ?)"
).run(oldIso, "lite", 100, 50, 50);
db.prepare(
"INSERT INTO compression_analytics (timestamp, mode, original_tokens, compressed_tokens, tokens_saved) VALUES (?, ?, ?, ?, ?)"
).run(recentIso, "lite", 100, 50, 50);
db.prepare(
"INSERT INTO daily_usage_summary (provider, model, date) VALUES (?, ?, ?)"
).run("openai", "gpt-test", oldDate);
@@ -98,6 +152,18 @@ test("resetUsageHistory: 'all' wipes usage_history, daily_usage_summary, and hou
seed();
assert.equal(countRows(db, "usage_history"), 2, "sanity: 2 usage_history rows seeded");
assert.equal(countRows(db, "call_logs"), 2, "sanity: 2 call_logs rows seeded");
assert.equal(
countRows(db, "request_detail_logs"),
2,
"sanity: 2 request_detail_logs rows seeded"
);
assert.equal(countRows(db, "proxy_logs"), 2, "sanity: 2 proxy_logs rows seeded");
assert.equal(
countRows(db, "compression_analytics"),
2,
"sanity: 2 compression_analytics rows seeded"
);
assert.equal(
countRows(db, "daily_usage_summary"),
2,
@@ -114,6 +180,18 @@ test("resetUsageHistory: 'all' wipes usage_history, daily_usage_summary, and hou
assert.equal(periodResult.errors, 0, "period reset should not report errors");
assert.equal(periodResult.deletedUsageHistory, 1, "should delete only the old usage_history row");
assert.equal(periodResult.deletedCallLogs, 1, "should delete only the old call_logs row");
assert.equal(
periodResult.deletedRequestDetailLogs,
1,
"should delete only the old request_detail_logs row"
);
assert.equal(periodResult.deletedProxyLogs, 1, "should delete only the old proxy_logs row");
assert.equal(
periodResult.deletedCompressionAnalytics,
1,
"should delete only the old compression_analytics row"
);
assert.equal(
periodResult.deletedDailySummary,
1,
@@ -124,9 +202,28 @@ test("resetUsageHistory: 'all' wipes usage_history, daily_usage_summary, and hou
1,
"should delete only the old hourly_usage_summary row"
);
assert.equal(periodResult.deleted, 3, "total deleted should sum the three tables");
assert.equal(periodResult.deleted, 7, "total deleted should sum the reset tables");
assert.equal(periodResult.deletedCallLogArtifacts, 1, "period reset should delete only old call artifact");
assert.equal(fs.existsSync(oldArtifactPath), false, "period reset should delete old call artifact");
assert.equal(fs.existsSync(recentArtifactPath), true, "period reset should preserve recent call artifact");
assert.equal(countRows(db, "provider_nodes"), 1, "provider config should survive reset");
assert.equal(countRows(db, "api_keys"), 1, "API keys should survive reset");
assert.equal(countRows(db, "combos"), 1, "combos should survive reset");
assert.equal(countRows(db, "usage_history"), 1, "recent usage_history row should survive");
assert.equal(countRows(db, "call_logs"), 1, "recent call_logs row should survive");
assert.equal(
countRows(db, "request_detail_logs"),
1,
"recent request_detail_logs row should survive"
);
assert.equal(countRows(db, "proxy_logs"), 1, "recent proxy_logs row should survive");
assert.equal(
countRows(db, "compression_analytics"),
1,
"recent compression_analytics row should survive"
);
assert.equal(
countRows(db, "daily_usage_summary"),
1,
@@ -152,6 +249,18 @@ test("resetUsageHistory: 'all' wipes usage_history, daily_usage_summary, and hou
assert.equal(allResult.errors, 0, "'all' reset should not report errors");
assert.equal(allResult.deletedUsageHistory, 1, "'all' should delete the remaining usage_history row");
assert.equal(allResult.deletedCallLogs, 1, "'all' should delete the remaining call_logs row");
assert.equal(
allResult.deletedRequestDetailLogs,
1,
"'all' should delete the remaining request_detail_logs row"
);
assert.equal(allResult.deletedProxyLogs, 1, "'all' should delete the remaining proxy_logs row");
assert.equal(
allResult.deletedCompressionAnalytics,
1,
"'all' should delete the remaining compression_analytics row"
);
assert.equal(
allResult.deletedDailySummary,
1,
@@ -162,10 +271,19 @@ test("resetUsageHistory: 'all' wipes usage_history, daily_usage_summary, and hou
1,
"'all' should delete the remaining hourly_usage_summary row"
);
assert.equal(allResult.deletedCallLogArtifacts, 1, "'all' should delete remaining call artifact");
assert.equal(fs.existsSync(recentArtifactPath), false, "'all' should delete recent call artifact");
assert.equal(countRows(db, "usage_history"), 0, "'all' should empty usage_history");
assert.equal(countRows(db, "call_logs"), 0, "'all' should empty call_logs");
assert.equal(countRows(db, "request_detail_logs"), 0, "'all' should empty request_detail_logs");
assert.equal(countRows(db, "proxy_logs"), 0, "'all' should empty proxy_logs");
assert.equal(countRows(db, "compression_analytics"), 0, "'all' should empty compression_analytics");
assert.equal(countRows(db, "daily_usage_summary"), 0, "'all' should empty daily_usage_summary");
assert.equal(countRows(db, "hourly_usage_summary"), 0, "'all' should empty hourly_usage_summary");
assert.equal(countRows(db, "provider_nodes"), 1, "provider config should still survive 'all'");
assert.equal(countRows(db, "api_keys"), 1, "API keys should still survive 'all'");
assert.equal(countRows(db, "combos"), 1, "combos should still survive 'all'");
// 3) An invalid period throws instead of silently doing nothing / deleting everything.
await assert.rejects(