Scope log retention deletion to selected hosts, not the whole table
api/logretention no longer deletes wholesale by age alone: a new
GET /logs/retention/hosts lists every host with matching records (plus
any configured retention floor), and preview/delete now require an
explicit, non-empty host list -- there is no "omitted host means every
host" shortcut server-side. Store's count/delete statements are
host-scoped (host IN (...)); Handler.partitionHosts checks the floor
per host instead of one global max, so a floor on one host never
blocks acting on other hosts requested in the same call. A request
that ends up fully or partially blocked still returns 200 with
blocked_hosts explaining why, rather than rejecting the whole call.
Settings' Log retention section is a host picker now: checkboxes with
per-host counts and a "protected Nd" badge where a floor applies,
"select all/none", and a confirm panel that names exactly which hosts
will be affected and which were skipped and why.
Verified live against real ClickHouse/Postgres and in-browser: three
hosts seeded, one protected by a 90-day floor -- a scoped delete
correctly removed the two open hosts' records, left the protected
host's untouched, and the response/UI both named it as skipped. Also
fixed a real spacing bug in the result message caught during that
browser pass (an adjacent {expr}{#if} with no source whitespace
between them rendered with no space either).
This commit is contained in:
+28
-6
@@ -483,16 +483,38 @@ export function setUserRole(id: string, role: string): Promise<LocalUser> {
|
||||
}
|
||||
|
||||
// --- log retention (owner/admin only, see api/logretention) -----------
|
||||
// Deletion is host-scoped, not wholesale: a caller must name which
|
||||
// hosts' logs to target (listRetentionHosts is how the UI discovers
|
||||
// what to offer), and api/logretention never treats an omitted host
|
||||
// list as "every host."
|
||||
|
||||
export type LogRetentionPreview = { count: number; cutoff: string };
|
||||
export type LogRetentionDeleteResult = { deleted_count: number; cutoff: string };
|
||||
export type BlockedHost = { host: string; protected_days: number };
|
||||
export type RetentionHost = { host: string; count: number; protected_days?: number };
|
||||
export type RetentionHostsResult = { hosts: RetentionHost[]; cutoff: string };
|
||||
export type LogRetentionPreview = { count: number; cutoff: string; hosts: string[]; blocked_hosts?: BlockedHost[] };
|
||||
export type LogRetentionDeleteResult = {
|
||||
deleted_count: number;
|
||||
cutoff: string;
|
||||
deleted_hosts: string[];
|
||||
blocked_hosts?: BlockedHost[];
|
||||
};
|
||||
|
||||
export function previewLogDeletion(olderThanHours: number): Promise<LogRetentionPreview> {
|
||||
return request(`/logs/retention/preview?older_than_hours=${olderThanHours}`, { credentials: 'include' });
|
||||
function hostsQuery(hosts: string[]): string {
|
||||
return hosts.map((h) => `host=${encodeURIComponent(h)}`).join('&');
|
||||
}
|
||||
|
||||
export function deleteLogsOlderThan(olderThanHours: number): Promise<LogRetentionDeleteResult> {
|
||||
return request(`/logs/retention?older_than_hours=${olderThanHours}`, {
|
||||
export function listRetentionHosts(olderThanHours: number): Promise<RetentionHostsResult> {
|
||||
return request(`/logs/retention/hosts?older_than_hours=${olderThanHours}`, { credentials: 'include' });
|
||||
}
|
||||
|
||||
export function previewLogDeletion(olderThanHours: number, hosts: string[]): Promise<LogRetentionPreview> {
|
||||
return request(`/logs/retention/preview?older_than_hours=${olderThanHours}&${hostsQuery(hosts)}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteLogsOlderThan(olderThanHours: number, hosts: string[]): Promise<LogRetentionDeleteResult> {
|
||||
return request(`/logs/retention?older_than_hours=${olderThanHours}&${hostsQuery(hosts)}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user