From 584084c09c5db48d93cd8ffb19893275d2e0f713 Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Wed, 7 Oct 2026 23:31:21 -0400 Subject: [PATCH] Exclude maintenance history from activity log --- src/lib/activity.ts | 2 ++ src/lib/server/activity.ts | 5 +++- src/lib/ui/activity-history.ts | 3 +- src/lib/ui/components/ActivityLog.svelte | 22 ++++++++++----- src/routes/activity/+page.server.ts | 2 +- src/routes/activity/+page.svelte | 4 +-- tests/unit/activity.test.ts | 36 +++++++++++++++++++++++- tests/unit/ui-activity.test.ts | 15 ++++++++++ 8 files changed, 76 insertions(+), 13 deletions(-) diff --git a/src/lib/activity.ts b/src/lib/activity.ts index b7cb220..573ce39 100644 --- a/src/lib/activity.ts +++ b/src/lib/activity.ts @@ -35,6 +35,7 @@ export interface ActivityRow { export interface ActivityQuery { excludeProvisioning?: boolean; + excludeMaintenance?: boolean; server?: string; host?: string; type?: ActivityType; @@ -69,6 +70,7 @@ export function parseActivityQuery(params: URLSearchParams): ActivityQuery { offset: integer(params.get('offset'), 0, 0, 1_000_000_000) }; if (params.get('excludeProvisioning') === 'true') query.excludeProvisioning = true; + if (params.get('excludeMaintenance') === 'true') query.excludeMaintenance = true; for (const key of ['server', 'host', 'q'] as const) { const value = params.get(key)?.trim().slice(0, key === 'q' ? 200 : 255); if (value) query[key] = value; diff --git a/src/lib/server/activity.ts b/src/lib/server/activity.ts index 21de7f8..18f02ff 100644 --- a/src/lib/server/activity.ts +++ b/src/lib/server/activity.ts @@ -130,6 +130,8 @@ normalized AS ( type SqlRow = Omit & { sources: string; route: string; recordId: string; secondaryHostId: string | null }; const sourceOrder: ActivitySource[] = ['maintenance', 'recovery', 'campaign', 'beta', 'autoscaling', 'provisioning', 'host']; +// Use the same membership rule for Maintenance history and its exclusion from the main log. +const maintenanceMembershipSql = "(instr(sources,'|maintenance|')>0 OR instr(sources,'|recovery|')>0)"; function present(row: SqlRow): ActivityRow { const sources = sourceOrder.filter(source => row.sources.includes(`|${source}|`)); const href = row.route === 'maintenance-job' ? `/maintenance?view=overview&job=${encodeURIComponent(row.recordId)}#job-${row.recordId}` @@ -154,12 +156,13 @@ export function activityHistory(input: ActivityQuery = {}, now = Date.now()): Ac // Keep lifecycle jobs in Provisioning, including jobs triggered by automation. // Apply this before totals and pagination; resource histories can still include them. if (query.excludeProvisioning) conditions.push("route <> 'provisioning' AND type <> 'restart'"); + if (query.excludeMaintenance) conditions.push(`NOT ${maintenanceMembershipSql}`); const bindings: Record = {}; for (const key of ['server', 'type', 'state'] as const) if (query[key]) { conditions.push(`${key}=@${key}`); bindings[key] = query[key]; } if (query.host) { conditions.push('(hostId=@host OR secondaryHostId=@host)'); bindings.host = query.host; } if (query.source) { // Maintenance is the user-facing umbrella; Recovery remains available as a narrower filter. - conditions.push(query.source === 'maintenance' ? "(instr(sources,'|maintenance|')>0 OR instr(sources,'|recovery|')>0)" : 'instr(sources,@source)>0'); + conditions.push(query.source === 'maintenance' ? maintenanceMembershipSql : 'instr(sources,@source)>0'); if (query.source !== 'maintenance') bindings.source = `|${query.source}|`; } if (query.days) { conditions.push('julianday(at)>=julianday(@since)'); bindings.since = new Date(now - query.days * 86_400_000).toISOString(); } diff --git a/src/lib/ui/activity-history.ts b/src/lib/ui/activity-history.ts index 610b98f..be99e78 100644 --- a/src/lib/ui/activity-history.ts +++ b/src/lib/ui/activity-history.ts @@ -1,7 +1,7 @@ import { parseActivityQuery, type ActivityQuery, type ActivitySource } from '../activity.js'; import { createTimeFormatter } from '../time-zone.js'; -export interface ActivityScope { server?: string; source?: ActivitySource; excludeProvisioning?: boolean } +export interface ActivityScope { server?: string; source?: ActivitySource; excludeProvisioning?: boolean; excludeMaintenance?: boolean } const keys = ['server', 'host', 'type', 'source', 'state', 'days', 'q', 'limit', 'offset'] as const; /** Scoped views always retain their server/source, including after reset or a pasted URL. */ @@ -10,6 +10,7 @@ export function activityQuery(url: URL, scope: ActivityScope = {}): ActivityQuer if (scope.server !== undefined) params.set('server', scope.server); if (scope.source !== undefined) params.set('source', scope.source); if (scope.excludeProvisioning !== undefined) params.set('excludeProvisioning', String(scope.excludeProvisioning)); + if (scope.excludeMaintenance !== undefined) params.set('excludeMaintenance', String(scope.excludeMaintenance)); return parseActivityQuery(params); } diff --git a/src/lib/ui/components/ActivityLog.svelte b/src/lib/ui/components/ActivityLog.svelte index 540d734..c6e5676 100644 --- a/src/lib/ui/components/ActivityLog.svelte +++ b/src/lib/ui/components/ActivityLog.svelte @@ -17,7 +17,7 @@ import Freshness from './Freshness.svelte'; import HistoricalServerIdentity from './HistoricalServerIdentity.svelte'; - let { server, source, initial, excludeProvisioning = false }: { server?: string; source?: ActivitySource; initial?: ActivityPage; excludeProvisioning?: boolean } = $props(); + let { server, source, initial, excludeProvisioning = false, excludeMaintenance = false }: { server?: string; source?: ActivitySource; initial?: ActivityPage; excludeProvisioning?: boolean; excludeMaintenance?: boolean } = $props(); const id = $props.id(); const headingId = `${id}-activity-heading`; let data = $state(untrack(() => initial)); @@ -27,14 +27,22 @@ let filtersOpen = $state(false); let checkedAt = $state(untrack(() => initial ? Date.now() : null)); let results = $state(); - let search = $state(untrack(() => activityQuery($page.url, { server, source, excludeProvisioning }).q ?? '')); + let search = $state(untrack(() => activityQuery($page.url, { server, source, excludeProvisioning, excludeMaintenance }).q ?? '')); let pendingUrl: URL | undefined; let navigationId = 0; - const filters = $derived(activityQuery($page.url, { server, source, excludeProvisioning })); - const apiUrl = $derived(activityApiHref($page.url, { server, source, excludeProvisioning })); + const filters = $derived(activityQuery($page.url, { server, source, excludeProvisioning, excludeMaintenance })); + const apiUrl = $derived(activityApiHref($page.url, { server, source, excludeProvisioning, excludeMaintenance })); let loadedUrl = $state(untrack(() => initial ? apiUrl : '')); - const typeOptions = $derived(Object.entries(activityTypes).filter(([key]) => !excludeProvisioning || !['create', 'start', 'restart', 'rebuild', 'stop', 'remove'].includes(key))); - const sourceOptions = $derived(Object.entries(activitySources).filter(([key]) => !excludeProvisioning || !['provisioning', 'autoscaling'].includes(key))); + const typeOptions = $derived(Object.entries(activityTypes).filter(([key]) => + (!excludeProvisioning || !['create', 'start', 'restart', 'rebuild', 'stop', 'remove'].includes(key)) + && (!excludeMaintenance || !['update', 'health'].includes(key)))); + const sourceOptions = $derived(Object.entries(activitySources).filter(([key]) => + (!excludeProvisioning || !['provisioning', 'autoscaling'].includes(key)) + && (!excludeMaintenance || !['maintenance', 'recovery'].includes(key)))); + const historyDescription = $derived(excludeMaintenance + ? 'File changes and host work appear here.' + : excludeProvisioning ? 'File changes, maintenance, and host work appear here.' + : 'Server operations, file changes, maintenance, and host work appear here.'); const filterCount = $derived([filters.type, filters.state, source === undefined ? filters.source : '', server === undefined ? filters.server : '', filters.host, filters.days, filters.q].filter(Boolean).length); const serverOptions = $derived.by(() => { const names = new Set([...$fleet.servers.map(item => item.name), ...(data?.rows.flatMap(row => row.server ? [row.server] : []) ?? [])]); @@ -125,7 +133,7 @@ {/each} - {:else if !error} + {:else if !error} {#snippet actions()}{#if filterCount}{/if}{/snippet} {/if} {#if data && !loading && loadedUrl === apiUrl && (data.total > 0 || data.offset > 0)} diff --git a/src/routes/activity/+page.server.ts b/src/routes/activity/+page.server.ts index a9e6a8b..bc8743b 100644 --- a/src/routes/activity/+page.server.ts +++ b/src/routes/activity/+page.server.ts @@ -2,5 +2,5 @@ import { parseActivityQuery } from '$lib/activity.js'; import { activityHistory } from '$lib/server/activity.js'; export function load({ url }: { url: URL }) { - return { activity: activityHistory({ ...parseActivityQuery(url.searchParams), excludeProvisioning: true }) }; + return { activity: activityHistory({ ...parseActivityQuery(url.searchParams), excludeProvisioning: true, excludeMaintenance: true }) }; } diff --git a/src/routes/activity/+page.svelte b/src/routes/activity/+page.svelte index 3cb57c6..e07e3f5 100644 --- a/src/routes/activity/+page.svelte +++ b/src/routes/activity/+page.svelte @@ -7,8 +7,8 @@ Activity · Electric Cherry
- - + +